Skip to content

Commit f9f0af4

Browse files
Merge pull request #207 from RedisInsight/feature/RIVS-296-Multiple_elements_to_lists
Feature/rivs 296 multiple elements to lists
2 parents 312444a + 086ca59 commit f9f0af4

File tree

25 files changed

+475
-133
lines changed

25 files changed

+475
-133
lines changed

src/webviews/src/modules/add-key/components/AddKeyList/AddKeyList.spec.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { render, screen, fireEvent } from 'testSrc/helpers'
44
import { AddKeyList, Props } from './AddKeyList'
55

66
const mockedProps = mock<Props>()
7+
const elementFindingRegex = /^element-\d+$/
78

89
describe('AddKeyList', () => {
910
it('should render', () => {
1011
expect(render(<AddKeyList {...instance(mockedProps)} />)).toBeTruthy()
1112
})
1213
it('should set value properly', () => {
1314
render(<AddKeyList {...instance(mockedProps)} />)
14-
const valueInput = screen.getByTestId('element')
15+
const valueInput = screen.getByTestId(elementFindingRegex)
1516
const value = 'list list list list list list '
1617
fireEvent.change(valueInput, { target: { value } })
1718
expect(valueInput).toHaveValue(value)
@@ -28,4 +29,28 @@ describe('AddKeyList', () => {
2829
const button = screen.getByTestId('btn-add') as HTMLButtonElement
2930
expect(button.disabled).toBe(false)
3031
})
32+
33+
it('should render add button', () => {
34+
render(<AddKeyList {...instance(mockedProps)} />)
35+
expect(screen.getByTestId('add-new-item')).toBeTruthy()
36+
})
37+
38+
it('should render one more element input after click add item', () => {
39+
render(<AddKeyList {...instance(mockedProps)} />)
40+
fireEvent.click(screen.getByTestId('add-new-item'))
41+
42+
expect(screen.getAllByTestId(elementFindingRegex)).toHaveLength(2)
43+
})
44+
45+
it('should clear the element after click clear button', () => {
46+
render(<AddKeyList {...instance(mockedProps)} />)
47+
const fieldName = screen.getByTestId('element-0')
48+
fireEvent.input(
49+
fieldName,
50+
{ target: { value: 'name' } },
51+
)
52+
fireEvent.click(screen.getByLabelText(/clear item/i))
53+
54+
expect(fieldName).toHaveValue('')
55+
})
3156
})

src/webviews/src/modules/add-key/components/AddKeyList/AddKeyList.tsx

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,39 @@ import React, { ChangeEvent, FormEvent, useState, useEffect } from 'react'
22
import * as l10n from '@vscode/l10n'
33
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'
44

5-
import { KeyTypes, AddListFormConfig as config } from 'uiSrc/constants'
5+
import { HEAD_DESTINATION, KeyTypes, ListElementDestination, TAIL_DESTINATION, AddListFormConfig as config } from 'uiSrc/constants'
66
import { getRequiredFieldsText, stringToBuffer } from 'uiSrc/utils'
77
import { Maybe } from 'uiSrc/interfaces'
88
import { useKeysApi, useKeysInContext } from 'uiSrc/modules/keys-tree/hooks/useKeys'
9-
import { InputText, Tooltip } from 'uiSrc/ui'
9+
import { InputText, Select, SelectOption, Tooltip } from 'uiSrc/ui'
1010
import { CreateListWithExpireDto } from 'uiSrc/modules/keys-tree/hooks/interface'
11+
import { AddItemsActions } from 'uiSrc/components'
12+
13+
import styles from '../../styles.module.scss'
1114

1215
export interface Props {
1316
keyName: string
1417
keyTTL: Maybe<number>
1518
onClose: (isCancelled?: boolean, keyType?: KeyTypes) => void
1619
}
1720

21+
const optionsDestinations: SelectOption[] = [
22+
{
23+
value: TAIL_DESTINATION,
24+
label: l10n.t('Push to tail'),
25+
testid: TAIL_DESTINATION,
26+
},
27+
{
28+
value: HEAD_DESTINATION,
29+
label: l10n.t('Push to head'),
30+
testid: HEAD_DESTINATION,
31+
},
32+
]
33+
1834
export const AddKeyList = (props: Props) => {
1935
const { keyName = '', keyTTL, onClose } = props
20-
const [element, setElement] = useState<string>('')
36+
const [elements, setElements] = useState<string[]>([''])
37+
const [destination, setDestination] = useState<ListElementDestination>(TAIL_DESTINATION)
2138
const [isFormValid, setIsFormValid] = useState<boolean>(false)
2239

2340
const keysApi = useKeysApi()
@@ -27,6 +44,28 @@ export const AddKeyList = (props: Props) => {
2744
setIsFormValid(keyName.length > 0)
2845
}, [keyName])
2946

47+
const addElement = () => {
48+
setElements([...elements, ''])
49+
}
50+
51+
const removeElement = (index: number) => {
52+
setElements(elements.filter((_el, i) => i !== index))
53+
}
54+
55+
const clearElement = (index: number) => {
56+
const newElements = [...elements]
57+
newElements[index] = ''
58+
setElements(newElements)
59+
}
60+
61+
const handleElementChange = (value: string, index: number) => {
62+
const newElements = [...elements]
63+
newElements[index] = value
64+
setElements(newElements)
65+
}
66+
67+
const isClearDisabled = (item:string) => elements.length === 1 && !item.length
68+
3069
const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {
3170
event.preventDefault()
3271
if (isFormValid) {
@@ -37,7 +76,8 @@ export const AddKeyList = (props: Props) => {
3776
const submitData = (): void => {
3877
const data: CreateListWithExpireDto = {
3978
keyName: stringToBuffer(keyName),
40-
element: stringToBuffer(element),
79+
destination,
80+
elements: elements.map((el) => stringToBuffer(el)),
4181
}
4282
if (keyTTL !== undefined) {
4383
data.expire = keyTTL
@@ -51,16 +91,43 @@ export const AddKeyList = (props: Props) => {
5191
<>
5292
<form onSubmit={onFormSubmit} className="key-footer-items-container pl-0 h-full">
5393
<h3 className="font-bold uppercase pb-3">{l10n.t('Element')}</h3>
54-
<InputText
55-
name="element"
56-
id="element"
57-
placeholder={config.element.placeholder}
58-
value={element}
59-
onChange={(e: ChangeEvent<HTMLInputElement>) =>
60-
setElement(e.target.value)}
61-
disabled={loading}
62-
data-testid="element"
63-
/>
94+
<div className="w-1/3 mr-2 mb-3">
95+
<Select
96+
position="below"
97+
options={optionsDestinations}
98+
containerClassName={styles.select}
99+
itemClassName={styles.selectOption}
100+
idSelected={destination}
101+
onChange={(value) => setDestination(value as ListElementDestination)}
102+
testid="destination-select"
103+
/>
104+
</div>
105+
{elements.map((item, index) => (
106+
<div key={index}>
107+
<div className="flex items-center mb-3">
108+
<InputText
109+
name={`element-${index}`}
110+
id={`element-${index}`}
111+
placeholder={config.element.placeholder}
112+
value={item}
113+
disabled={loading}
114+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
115+
handleElementChange(e.target.value, index)}
116+
data-testid={`element-${index}`}
117+
/>
118+
<AddItemsActions
119+
id={index}
120+
index={index}
121+
length={elements.length}
122+
addItem={addElement}
123+
removeItem={removeElement}
124+
clearItemValues={clearElement}
125+
clearIsDisabled={isClearDisabled(item)}
126+
disabled={loading}
127+
/>
128+
</div>
129+
</div>
130+
))}
64131
<button type="submit" className="hidden">
65132
{l10n.t('Submit')}
66133
</button>

src/webviews/src/modules/add-key/styles.module.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,18 @@
2222
border-top: 1px solid;
2323
opacity: .5;
2424
}
25+
26+
.select {
27+
@apply h-full w-full bg-vscode-dropdown-background ;
28+
border: calc(var(--border-width)* 1px) solid var(--dropdown-border);
29+
&::part(control) {
30+
@apply border-vscode-dropdown-background;
31+
}
32+
&::part(listbox) {
33+
@apply mt-4;
34+
}
35+
}
36+
37+
.select {
38+
height: 43px !important;
39+
}

src/webviews/src/modules/cli/hooks/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export interface CreateSetWithExpireDto extends KeyWithExpireDto {
112112
}
113113

114114
export interface CreateListWithExpireDto extends KeyWithExpireDto {
115-
element: RedisString
115+
elements: RedisString[]
116116
}
117117

118118
export interface HashFieldDto {

src/webviews/src/modules/key-details/components/list-details/add-list-elements/AddListElements.spec.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { fireEvent, render, screen } from 'testSrc/helpers'
55
import { AddListElements, Props } from './AddListElements'
66

77
const mockedProps = mock<Props>()
8+
const elementFindingRegex = /^element-\d+$/
89

910
describe('AddListElements', () => {
1011
it('should render', () => {
@@ -13,7 +14,7 @@ describe('AddListElements', () => {
1314

1415
it('should set elements input properly', () => {
1516
render(<AddListElements {...instance(mockedProps)} />)
16-
const elementsInput = screen.getByTestId('elements-input')
17+
const elementsInput = screen.getByTestId(elementFindingRegex)
1718
fireEvent.change(
1819
elementsInput,
1920
{ target: { value: '123' } },
@@ -27,4 +28,28 @@ describe('AddListElements', () => {
2728
expect(screen.queryByTestId(HEAD_DESTINATION)).toBeInTheDocument()
2829
expect(screen.queryByTestId(TAIL_DESTINATION)).toBeInTheDocument()
2930
})
31+
32+
it('should render add button', () => {
33+
render(<AddListElements {...instance(mockedProps)} />)
34+
expect(screen.getByTestId('add-new-item')).toBeTruthy()
35+
})
36+
37+
it('should render one more element input after click add item', () => {
38+
render(<AddListElements {...instance(mockedProps)} />)
39+
fireEvent.click(screen.getByTestId('add-new-item'))
40+
41+
expect(screen.getAllByTestId(elementFindingRegex)).toHaveLength(2)
42+
})
43+
44+
it('should clear the element after click clear button', () => {
45+
render(<AddListElements {...instance(mockedProps)} />)
46+
const fieldName = screen.getByTestId('element-0')
47+
fireEvent.input(
48+
fieldName,
49+
{ target: { value: 'name' } },
50+
)
51+
fireEvent.click(screen.getByLabelText(/clear item/i))
52+
53+
expect(fieldName).toHaveValue('')
54+
})
3055
})

src/webviews/src/modules/key-details/components/list-details/add-list-elements/AddListElements.tsx

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { TelemetryEvent, sendEventTelemetry, stringToBuffer } from 'uiSrc/utils'
1313

1414
import { useDatabasesStore, useSelectedKeyStore } from 'uiSrc/store'
1515
import { InputText, Select, SelectOption } from 'uiSrc/ui'
16+
import { AddItemsActions } from 'uiSrc/components'
1617
import { PushElementToListDto } from '../hooks/interface'
1718
import { insertListElementsAction, useListStore } from '../hooks/useListStore'
1819
import styles from '../styles.module.scss'
@@ -37,7 +38,7 @@ const optionsDestinations: SelectOption[] = [
3738
const AddListElements = (props: Props) => {
3839
const { closePanel } = props
3940

40-
const [element, setElement] = useState<string>('')
41+
const [elements, setElements] = useState<string[]>([''])
4142
const [destination, setDestination] = useState<ListElementDestination>(TAIL_DESTINATION)
4243
const databaseId = useDatabasesStore((state) => state.connectedDatabase?.id)
4344

@@ -58,15 +59,37 @@ const AddListElements = (props: Props) => {
5859
eventData: {
5960
databaseId,
6061
keyType: KeyTypes.List,
61-
numberOfAdded: 1,
62+
numberOfAdded: elements.length,
6263
},
6364
})
6465
}
6566

67+
const addElement = () => {
68+
setElements([...elements, ''])
69+
}
70+
71+
const removeElement = (index: number) => {
72+
setElements(elements.filter((_el, i) => i !== index))
73+
}
74+
75+
const clearElement = (index: number) => {
76+
const newElements = [...elements]
77+
newElements[index] = ''
78+
setElements(newElements)
79+
}
80+
81+
const handleElementChange = (value: string, index: number) => {
82+
const newElements = [...elements]
83+
newElements[index] = value
84+
setElements(newElements)
85+
}
86+
87+
const isClearDisabled = (item:string) => elements.length === 1 && !item.length
88+
6689
const submitData = (): void => {
6790
const data: PushElementToListDto = {
6891
keyName: selectedKey!,
69-
element: stringToBuffer(element),
92+
elements: elements.map((el) => stringToBuffer(el)),
7093
destination,
7194
}
7295
insertListElementsAction(data, onSuccessAdded)
@@ -76,8 +99,8 @@ const AddListElements = (props: Props) => {
7699
<>
77100
<div className="key-footer-items-container">
78101
<div className="flex items-center mb-3">
79-
<div className="flex grow">
80-
<div className="w-1/3 mr-2">
102+
<div className="flex-column grow">
103+
<div className="w-1/3 mr-2 mb-3">
81104
<Select
82105
position="below"
83106
options={optionsDestinations}
@@ -88,18 +111,33 @@ const AddListElements = (props: Props) => {
88111
testid="destination-select"
89112
/>
90113
</div>
91-
<div className="w-2/3">
92-
<InputText
93-
name={config.element.name}
94-
id={config.element.name}
95-
placeholder={config.element.placeholder}
96-
value={element}
97-
autoComplete="off"
98-
onChange={(e: ChangeEvent<HTMLInputElement>) => setElement(e.target.value)}
99-
data-testid="elements-input"
100-
inputRef={elementInput}
101-
/>
102-
</div>
114+
{elements.map((item, index) => (
115+
<div key={index}>
116+
<div className="flex items-center mb-3">
117+
<InputText
118+
name={`element-${index}`}
119+
id={`element-${index}`}
120+
placeholder={config.element.placeholder}
121+
value={item}
122+
disabled={loading}
123+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
124+
handleElementChange(e.target.value, index)}
125+
inputRef={index === elements.length - 1 ? elementInput : null}
126+
data-testid={`element-${index}`}
127+
/>
128+
<AddItemsActions
129+
id={index}
130+
index={index}
131+
length={elements.length}
132+
addItem={addElement}
133+
removeItem={removeElement}
134+
clearItemValues={clearElement}
135+
clearIsDisabled={isClearDisabled(item)}
136+
disabled={loading}
137+
/>
138+
</div>
139+
</div>
140+
))}
103141
</div>
104142
</div>
105143
</div>

src/webviews/src/modules/key-details/components/list-details/hooks/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export interface DeleteListElementsDto extends KeyDto {
6161
}
6262

6363
export interface PushElementToListDto extends KeyDto {
64-
element: RedisString
64+
elements: RedisString[]
6565
destination: ListElementDestination
6666
}
6767

src/webviews/src/modules/key-details/components/list-details/hooks/tests/useListStore.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ describe('async', () => {
165165

166166
insertListElementsAction({
167167
keyName: constants.KEY_NAME_4,
168-
element: constants.KEY_4_ELEMENT,
168+
elements: [constants.KEY_4_ELEMENT],
169169
destination: ListElementDestination.Head,
170170
})
171171
await waitForStack()

src/webviews/src/modules/key-details/components/list-details/styles.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
.select {
66
@apply h-full w-full bg-vscode-dropdown-background ;
7-
7+
border: calc(var(--border-width)* 1px) solid var(--dropdown-border);
88
&::part(control) {
99
@apply border-vscode-dropdown-background;
1010
}

0 commit comments

Comments
 (0)