Skip to content

Commit 79eac72

Browse files
committed
feat: migrate the dynamic addition to action based addition in KeyValueTable
1 parent 95606a3 commit 79eac72

File tree

1 file changed

+125
-83
lines changed

1 file changed

+125
-83
lines changed

src/Shared/Components/KeyValueTable/KeyValueTable.component.tsx

Lines changed: 125 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { followCursor } from 'tippy.js'
2222
import { ReactComponent as ICArrowDown } from '@Icons/ic-sort-arrow-down.svg'
2323
import { ReactComponent as ICClose } from '@Icons/ic-close.svg'
2424
import { ReactComponent as ICCross } from '@Icons/ic-cross.svg'
25+
import { ReactComponent as ICAdd } from '@Icons/ic-add.svg'
2526
import { ConditionalWrap, ResizableTagTextArea, SortingOrder, useStateFilters } from '@Common/index'
2627
import { stringComparatorBySortOrder } from '@Shared/Helpers'
2728
import { DEFAULT_SECRET_PLACEHOLDER } from '@Shared/constants'
@@ -64,10 +65,13 @@ export const KeyValueTable = <K extends string>({
6465

6566
// STATES
6667
const [updatedRows, setUpdatedRows] = useState<KeyValueRow<K>[]>(rows)
68+
/** State to trigger useEffect to trigger autoFocus */
6769
const [newRowAdded, setNewRowAdded] = useState(false)
6870

6971
/** Boolean determining if table has rows. */
7072
const hasRows = (!readOnly && !isAdditionNotAllowed) || !!updatedRows.length
73+
// TODO: See null checks
74+
const isFirstRowEmpty = !updatedRows[0]?.data[firstHeaderKey].value && !updatedRows[0]?.data[secondHeaderKey].value
7175

7276
// HOOKS
7377
const { sortBy, sortOrder, handleSorting } = useStateFilters({
@@ -85,37 +89,6 @@ export const KeyValueTable = <K extends string>({
8589
valueTextAreaRef.current = updatedRows.reduce((acc, curr) => ({ ...acc, [curr.id]: createRef() }), {})
8690
}
8791

88-
useEffect(() => {
89-
const sortedRows = [...updatedRows]
90-
sortedRows.sort((a, b) => stringComparatorBySortOrder(a.data[sortBy].value, b.data[sortBy].value, sortOrder))
91-
setUpdatedRows(sortedRows)
92-
}, [sortOrder])
93-
94-
useEffect(() => {
95-
const firstRow = updatedRows?.[0]
96-
if (firstRow && newRowAdded) {
97-
setNewRowAdded(false)
98-
99-
if (
100-
!firstRow.data[secondHeaderKey].value &&
101-
keyTextAreaRef.current[firstRow.id].current &&
102-
valueTextAreaRef.current[firstRow.id].current
103-
) {
104-
keyTextAreaRef.current[firstRow.id].current.focus()
105-
}
106-
if (
107-
!firstRow.data[firstHeaderKey].value &&
108-
keyTextAreaRef.current[firstRow.id].current &&
109-
valueTextAreaRef.current[firstRow.id].current
110-
) {
111-
valueTextAreaRef.current[firstRow.id].current.focus()
112-
}
113-
}
114-
}, [newRowAdded])
115-
116-
// METHODS
117-
const onSortBtnClick = () => handleSorting(sortBy)
118-
11992
const checkAllRowsAreValid = (_rows: KeyValueRow<K>[]) => {
12093
const isValid = _rows.every(
12194
({ data: _data }) =>
@@ -126,40 +99,106 @@ export const KeyValueTable = <K extends string>({
12699
return isValid
127100
}
128101

129-
const onNewRowAdd = (key: K) => (e: React.ChangeEvent<HTMLTextAreaElement>) => {
130-
const { value } = e.target
131-
102+
const getEmptyRow = (): KeyValueRow<K> => {
132103
const id = (Date.now() * Math.random()).toString(16)
133104
const data = {
134105
data: {
135106
[firstHeaderKey]: {
136-
value: key === firstHeaderKey ? value : '',
107+
value: '',
137108
},
138109
[secondHeaderKey]: {
139-
value: key === secondHeaderKey ? value : '',
110+
value: '',
140111
},
141112
},
142113
id,
143114
} as KeyValueRow<K>
144-
const editedRows = [data, ...updatedRows]
115+
116+
return data
117+
}
118+
119+
const handleAddNewRow = () => {
120+
const data = getEmptyRow()
121+
const editedRows = [...updatedRows, data]
122+
123+
const { id } = data
145124

146125
onError?.(!checkAllRowsAreValid(editedRows))
147126
setNewRowAdded(true)
148127
setUpdatedRows(editedRows)
149-
onChange?.(id, key, value)
150128

151129
keyTextAreaRef.current = {
152-
...keyTextAreaRef.current,
153-
[id]: createRef(),
130+
...(keyTextAreaRef.current || {}),
131+
[id as string]: createRef(),
154132
}
155133
valueTextAreaRef.current = {
156-
...valueTextAreaRef.current,
157-
[id]: createRef(),
134+
...(valueTextAreaRef.current || {}),
135+
[id as string]: createRef(),
158136
}
159137
}
160138

139+
useEffect(() => {
140+
if (!readOnly && !isAdditionNotAllowed && !updatedRows.length) {
141+
handleAddNewRow()
142+
}
143+
}, [])
144+
145+
useEffect(() => {
146+
setUpdatedRows((prevRows) => {
147+
const sortedRows = [...prevRows]
148+
sortedRows.sort((a, b) =>
149+
stringComparatorBySortOrder(a.data[sortBy].value, b.data[sortBy].value, sortOrder),
150+
)
151+
return sortedRows
152+
})
153+
}, [sortOrder])
154+
155+
useEffect(() => {
156+
const lastRow = updatedRows?.length ? updatedRows[updatedRows.length - 1] : null
157+
if (lastRow && newRowAdded) {
158+
setNewRowAdded(false)
159+
160+
if (
161+
!lastRow.data[firstHeaderKey].value &&
162+
keyTextAreaRef.current[lastRow.id].current &&
163+
valueTextAreaRef.current[lastRow.id].current
164+
) {
165+
valueTextAreaRef.current[lastRow.id].current.focus()
166+
}
167+
if (
168+
!lastRow.data[secondHeaderKey].value &&
169+
keyTextAreaRef.current[lastRow.id].current &&
170+
valueTextAreaRef.current[lastRow.id].current
171+
) {
172+
keyTextAreaRef.current[lastRow.id].current.focus()
173+
}
174+
}
175+
}, [newRowAdded])
176+
177+
// METHODS
178+
const onSortBtnClick = () => handleSorting(sortBy)
179+
161180
const onRowDelete = (row: KeyValueRow<K>) => () => {
162181
const remainingRows = updatedRows.filter(({ id }) => id !== row.id)
182+
183+
if (remainingRows.length === 0 && !isAdditionNotAllowed) {
184+
const emptyRowData = getEmptyRow()
185+
const { id } = emptyRowData
186+
187+
setNewRowAdded(true)
188+
onError?.(!checkAllRowsAreValid([emptyRowData]))
189+
setUpdatedRows([emptyRowData])
190+
191+
keyTextAreaRef.current = {
192+
[id as string]: createRef(),
193+
}
194+
valueTextAreaRef.current = {
195+
[id as string]: createRef(),
196+
}
197+
198+
onDelete?.(row.id)
199+
return
200+
}
201+
163202
onError?.(!checkAllRowsAreValid(remainingRows))
164203
setUpdatedRows(remainingRows)
165204

@@ -203,66 +242,68 @@ export const KeyValueTable = <K extends string>({
203242
}
204243
}
205244

245+
const renderFirstHeader = (key: K, label: string, className: string) => (
246+
<div
247+
key={key}
248+
className={`bcn-50 py-8 px-12 flexbox dc__content-space dc__align-items-center ${updatedRows.length || (!readOnly && !isAdditionNotAllowed) ? 'dc__top-left-radius' : 'dc__left-radius-4'} ${className || ''}`}
249+
>
250+
{isSortable ? (
251+
<button
252+
type="button"
253+
className="cn-9 fs-13 lh-20-imp fw-6 flexbox dc__align-items-center dc__gap-2"
254+
onClick={onSortBtnClick}
255+
>
256+
{label}
257+
<ICArrowDown
258+
className="icon-dim-16 dc__no-shrink scn-7 rotate cursor"
259+
style={{
260+
['--rotateBy' as string]: sortOrder === SortingOrder.ASC ? '0deg' : '180deg',
261+
}}
262+
/>
263+
</button>
264+
) : (
265+
<div
266+
className={`cn-9 fs-13 lh-20 fw-6 flexbox dc__align-items-center dc__content-space dc__gap-2 ${hasRows ? 'dc__top-left-radius' : 'dc__left-radius-4'}`}
267+
>
268+
{label}
269+
{/* TODO: Test this */}
270+
{!!headerComponent && headerComponent}
271+
</div>
272+
)}
273+
274+
<button type="button" className="dc__transparent p-0 flex dc__gap-4" onClick={handleAddNewRow}>
275+
<ICAdd className="icon-dim-12 fcb-5 dc__no-shrink" />
276+
<span className="cb-5 fs-12 fw-6 lh-20">Add</span>
277+
</button>
278+
</div>
279+
)
280+
206281
return (
207282
<>
208283
<div className={`bcn-2 p-1 ${hasRows ? 'dc__top-radius-4' : 'br-4'}`}>
209284
<div className="key-value-table two-columns w-100 bcn-1 br-4">
285+
{/* HEADER */}
210286
<div className="key-value-table__row">
211287
{headers.map(({ key, label, className }) =>
212-
isSortable && key === firstHeaderKey ? (
213-
<button
214-
key={key}
215-
type="button"
216-
className={`bcn-50 dc__unset-button-styles cn-9 fs-13 lh-20-imp py-8 px-12 fw-6 flexbox dc__align-items-center dc__gap-2 ${updatedRows.length || (!readOnly && !isAdditionNotAllowed) ? 'dc__top-left-radius' : 'dc__left-radius-4'} ${className || ''}`}
217-
onClick={onSortBtnClick}
218-
>
219-
{label}
220-
<ICArrowDown
221-
className="icon-dim-16 scn-7 rotate cursor"
222-
style={{
223-
['--rotateBy' as string]:
224-
sortOrder === SortingOrder.ASC ? '0deg' : '180deg',
225-
}}
226-
/>
227-
</button>
288+
key === firstHeaderKey ? (
289+
renderFirstHeader(key, label, className)
228290
) : (
229291
<div
230292
key={key}
231293
className={`bcn-50 cn-9 fs-13 lh-20 py-8 px-12 fw-6 flexbox dc__align-items-center dc__content-space dc__gap-2 ${key === firstHeaderKey ? `${hasRows ? 'dc__top-left-radius' : 'dc__left-radius-4'}` : `${hasRows ? 'dc__top-right-radius' : 'dc__right-radius-4'}`} ${className || ''}`}
232294
>
233295
{label}
296+
{/* TODO: Test this */}
234297
{!!headerComponent && headerComponent}
235298
</div>
236299
),
237300
)}
238301
</div>
239302
</div>
240303
</div>
304+
241305
{hasRows && (
242306
<div className="bcn-2 px-1 pb-1 dc__bottom-radius-4">
243-
{!readOnly && !isAdditionNotAllowed && (
244-
<div
245-
className={`key-value-table two-columns-top-row bcn-1 ${updatedRows.length ? 'pb-1' : 'dc__bottom-radius-4'}`}
246-
>
247-
<div className="key-value-table__row">
248-
{headers.map(({ key }) => (
249-
<div
250-
key={key}
251-
className={`key-value-table__cell bcn-0 flex dc__overflow-auto ${(!updatedRows.length && (key === firstHeaderKey ? 'dc__bottom-left-radius' : 'dc__bottom-right-radius')) || ''}`}
252-
>
253-
<textarea
254-
ref={key === firstHeaderKey ? inputRowRef : undefined}
255-
className="key-value-table__cell-input key-value-table__cell-input--add placeholder-cn5 py-8 px-12 cn-9 lh-20 fs-13 fw-4 dc__no-border-radius"
256-
value=""
257-
rows={1}
258-
placeholder={placeholder[key]}
259-
onChange={onNewRowAdd(key)}
260-
/>
261-
</div>
262-
))}
263-
</div>
264-
</div>
265-
)}
266307
{!!updatedRows.length && (
267308
<div
268309
className={`key-value-table w-100 bcn-1 dc__bottom-radius-4 ${!readOnly ? 'three-columns' : 'two-columns'}`}
@@ -334,12 +375,13 @@ export const KeyValueTable = <K extends string>({
334375
{!readOnly && (
335376
<button
336377
type="button"
337-
className="key-value-table__row-delete-btn dc__unset-button-styles dc__align-self-stretch dc__no-shrink flex py-10 px-8 bcn-0 dc__hover-n50 dc__tab-focus"
378+
className={`key-value-table__row-delete-btn dc__unset-button-styles dc__align-self-stretch dc__no-shrink flex py-10 px-8 bcn-0 dc__hover-n50 dc__tab-focus ${updatedRows.length === 1 && isFirstRowEmpty ? 'dc__disabled' : ''}`}
338379
onClick={onRowDelete(row)}
380+
disabled={updatedRows.length === 1 && isFirstRowEmpty}
339381
>
340382
<ICCross
341383
aria-label="delete-row"
342-
className="icon-dim-16 fcn-4 dc__align-self-start cursor"
384+
className="icon-dim-16 fcn-4 dc__align-self-start"
343385
/>
344386
</button>
345387
)}

0 commit comments

Comments
 (0)