@@ -22,6 +22,7 @@ import { followCursor } from 'tippy.js'
22
22
import { ReactComponent as ICArrowDown } from '@Icons/ic-sort-arrow-down.svg'
23
23
import { ReactComponent as ICClose } from '@Icons/ic-close.svg'
24
24
import { ReactComponent as ICCross } from '@Icons/ic-cross.svg'
25
+ import { ReactComponent as ICAdd } from '@Icons/ic-add.svg'
25
26
import { ConditionalWrap , ResizableTagTextArea , SortingOrder , useStateFilters } from '@Common/index'
26
27
import { stringComparatorBySortOrder } from '@Shared/Helpers'
27
28
import { DEFAULT_SECRET_PLACEHOLDER } from '@Shared/constants'
@@ -64,10 +65,13 @@ export const KeyValueTable = <K extends string>({
64
65
65
66
// STATES
66
67
const [ updatedRows , setUpdatedRows ] = useState < KeyValueRow < K > [ ] > ( rows )
68
+ /** State to trigger useEffect to trigger autoFocus */
67
69
const [ newRowAdded , setNewRowAdded ] = useState ( false )
68
70
69
71
/** Boolean determining if table has rows. */
70
72
const hasRows = ( ! readOnly && ! isAdditionNotAllowed ) || ! ! updatedRows . length
73
+ // TODO: See null checks
74
+ const isFirstRowEmpty = ! updatedRows [ 0 ] ?. data [ firstHeaderKey ] . value && ! updatedRows [ 0 ] ?. data [ secondHeaderKey ] . value
71
75
72
76
// HOOKS
73
77
const { sortBy, sortOrder, handleSorting } = useStateFilters ( {
@@ -85,37 +89,6 @@ export const KeyValueTable = <K extends string>({
85
89
valueTextAreaRef . current = updatedRows . reduce ( ( acc , curr ) => ( { ...acc , [ curr . id ] : createRef ( ) } ) , { } )
86
90
}
87
91
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
-
119
92
const checkAllRowsAreValid = ( _rows : KeyValueRow < K > [ ] ) => {
120
93
const isValid = _rows . every (
121
94
( { data : _data } ) =>
@@ -126,40 +99,106 @@ export const KeyValueTable = <K extends string>({
126
99
return isValid
127
100
}
128
101
129
- const onNewRowAdd = ( key : K ) => ( e : React . ChangeEvent < HTMLTextAreaElement > ) => {
130
- const { value } = e . target
131
-
102
+ const getEmptyRow = ( ) : KeyValueRow < K > => {
132
103
const id = ( Date . now ( ) * Math . random ( ) ) . toString ( 16 )
133
104
const data = {
134
105
data : {
135
106
[ firstHeaderKey ] : {
136
- value : key === firstHeaderKey ? value : '' ,
107
+ value : '' ,
137
108
} ,
138
109
[ secondHeaderKey ] : {
139
- value : key === secondHeaderKey ? value : '' ,
110
+ value : '' ,
140
111
} ,
141
112
} ,
142
113
id,
143
114
} 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
145
124
146
125
onError ?.( ! checkAllRowsAreValid ( editedRows ) )
147
126
setNewRowAdded ( true )
148
127
setUpdatedRows ( editedRows )
149
- onChange ?.( id , key , value )
150
128
151
129
keyTextAreaRef . current = {
152
- ...keyTextAreaRef . current ,
153
- [ id ] : createRef ( ) ,
130
+ ...( keyTextAreaRef . current || { } ) ,
131
+ [ id as string ] : createRef ( ) ,
154
132
}
155
133
valueTextAreaRef . current = {
156
- ...valueTextAreaRef . current ,
157
- [ id ] : createRef ( ) ,
134
+ ...( valueTextAreaRef . current || { } ) ,
135
+ [ id as string ] : createRef ( ) ,
158
136
}
159
137
}
160
138
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
+
161
180
const onRowDelete = ( row : KeyValueRow < K > ) => ( ) => {
162
181
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
+
163
202
onError ?.( ! checkAllRowsAreValid ( remainingRows ) )
164
203
setUpdatedRows ( remainingRows )
165
204
@@ -203,66 +242,68 @@ export const KeyValueTable = <K extends string>({
203
242
}
204
243
}
205
244
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
+
206
281
return (
207
282
< >
208
283
< div className = { `bcn-2 p-1 ${ hasRows ? 'dc__top-radius-4' : 'br-4' } ` } >
209
284
< div className = "key-value-table two-columns w-100 bcn-1 br-4" >
285
+ { /* HEADER */ }
210
286
< div className = "key-value-table__row" >
211
287
{ 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 )
228
290
) : (
229
291
< div
230
292
key = { key }
231
293
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 || '' } ` }
232
294
>
233
295
{ label }
296
+ { /* TODO: Test this */ }
234
297
{ ! ! headerComponent && headerComponent }
235
298
</ div >
236
299
) ,
237
300
) }
238
301
</ div >
239
302
</ div >
240
303
</ div >
304
+
241
305
{ hasRows && (
242
306
< 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
- ) }
266
307
{ ! ! updatedRows . length && (
267
308
< div
268
309
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>({
334
375
{ ! readOnly && (
335
376
< button
336
377
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' : '' } ` }
338
379
onClick = { onRowDelete ( row ) }
380
+ disabled = { updatedRows . length === 1 && isFirstRowEmpty }
339
381
>
340
382
< ICCross
341
383
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"
343
385
/>
344
386
</ button >
345
387
) }
0 commit comments