14
14
* limitations under the License.
15
15
*/
16
16
17
- import { createRef , useEffect , useRef , useState , ReactElement , Fragment } from 'react'
17
+ import { createRef , useEffect , useRef , useState , ReactElement , Fragment , useMemo } from 'react'
18
18
import Tippy from '@tippyjs/react'
19
19
// eslint-disable-next-line import/no-extraneous-dependencies
20
20
import { followCursor } from 'tippy.js'
@@ -29,6 +29,7 @@ import { DEFAULT_SECRET_PLACEHOLDER } from '@Shared/constants'
29
29
30
30
import { KeyValueRow , KeyValueTableProps } from './KeyValueTable.types'
31
31
import './KeyValueTable.scss'
32
+ import { DUPLICATE_KEYS_VALIDATION_MESSAGE } from './constants'
32
33
33
34
const renderWithReadOnlyTippy = ( children : ReactElement ) => (
34
35
< Tippy
@@ -54,9 +55,10 @@ export const KeyValueTable = <K extends string>({
54
55
isAdditionNotAllowed,
55
56
readOnly,
56
57
showError,
57
- validationSchema,
58
- errorMessages = [ ] ,
58
+ validationSchema : parentValidationSchema ,
59
+ errorMessages : parentErrorMessages = [ ] ,
59
60
onError,
61
+ validateDuplicateKeys = false ,
60
62
} : KeyValueTableProps < K > ) => {
61
63
// CONSTANTS
62
64
const { headers, rows } = config
@@ -89,11 +91,60 @@ export const KeyValueTable = <K extends string>({
89
91
valueTextAreaRef . current = updatedRows . reduce ( ( acc , curr ) => ( { ...acc , [ curr . id ] : createRef ( ) } ) , { } )
90
92
}
91
93
92
- const checkAllRowsAreValid = ( _rows : KeyValueRow < K > [ ] ) => {
93
- const isValid = _rows . every (
94
+ const updatedRowsKeysFrequency : Record < string , number > = useMemo (
95
+ ( ) =>
96
+ updatedRows . reduce (
97
+ ( acc , curr ) => {
98
+ const currentKey = curr . data [ firstHeaderKey ] . value
99
+ if ( currentKey ) {
100
+ acc [ currentKey ] = ( acc [ currentKey ] || 0 ) + 1
101
+ }
102
+ return acc
103
+ } ,
104
+ { } as Record < string , number > ,
105
+ ) ,
106
+ [ updatedRows ] ,
107
+ )
108
+
109
+ const validationSchema : typeof parentValidationSchema = ( value , key ) => {
110
+ if ( validateDuplicateKeys && updatedRowsKeysFrequency [ value ] > 1 ) {
111
+ return false
112
+ }
113
+
114
+ if ( parentValidationSchema ) {
115
+ return parentValidationSchema ( value , key )
116
+ }
117
+
118
+ return true
119
+ }
120
+
121
+ const checkAllRowsAreValid = ( editedRows : KeyValueRow < K > [ ] ) => {
122
+ if ( validateDuplicateKeys ) {
123
+ const { isAnyKeyDuplicated } = editedRows . reduce (
124
+ ( acc , curr ) => {
125
+ const { keysFrequency } = acc
126
+ const currentKey = curr . data [ firstHeaderKey ] . value
127
+ if ( currentKey ) {
128
+ keysFrequency [ currentKey ] = ( keysFrequency [ currentKey ] || 0 ) + 1
129
+ }
130
+
131
+ return {
132
+ isAnyKeyDuplicated : acc . isAnyKeyDuplicated || keysFrequency [ currentKey ] > 1 ,
133
+ keysFrequency,
134
+ }
135
+ } ,
136
+ { isAnyKeyDuplicated : false , keysFrequency : { } as Record < string , number > } ,
137
+ )
138
+
139
+ if ( isAnyKeyDuplicated ) {
140
+ return false
141
+ }
142
+ }
143
+
144
+ const isValid = editedRows . every (
94
145
( { data : _data } ) =>
95
- validationSchema ?. ( _data [ firstHeaderKey ] . value , firstHeaderKey ) &&
96
- validationSchema ?. ( _data [ secondHeaderKey ] . value , secondHeaderKey ) ,
146
+ validationSchema ( _data [ firstHeaderKey ] . value , firstHeaderKey ) &&
147
+ validationSchema ( _data [ secondHeaderKey ] . value , secondHeaderKey ) ,
97
148
)
98
149
99
150
return isValid
@@ -118,7 +169,7 @@ export const KeyValueTable = <K extends string>({
118
169
119
170
const handleAddNewRow = ( ) => {
120
171
const data = getEmptyRow ( )
121
- const editedRows = [ ...updatedRows , data ]
172
+ const editedRows = [ data , ...updatedRows ]
122
173
123
174
const { id } = data
124
175
@@ -153,23 +204,24 @@ export const KeyValueTable = <K extends string>({
153
204
} , [ sortOrder ] )
154
205
155
206
useEffect ( ( ) => {
156
- const lastRow = updatedRows ?. length ? updatedRows [ updatedRows . length - 1 ] : null
157
- if ( lastRow && newRowAdded ) {
207
+ const firstRow = updatedRows ?. [ 0 ]
208
+
209
+ if ( firstRow && newRowAdded ) {
158
210
setNewRowAdded ( false )
159
211
160
212
if (
161
- ! lastRow . data [ firstHeaderKey ] . value &&
162
- keyTextAreaRef . current [ lastRow . id ] . current &&
163
- valueTextAreaRef . current [ lastRow . id ] . current
213
+ ! firstRow . data [ firstHeaderKey ] . value &&
214
+ keyTextAreaRef . current [ firstRow . id ] . current &&
215
+ valueTextAreaRef . current [ firstRow . id ] . current
164
216
) {
165
- valueTextAreaRef . current [ lastRow . id ] . current . focus ( )
217
+ valueTextAreaRef . current [ firstRow . id ] . current . focus ( )
166
218
}
167
219
if (
168
- ! lastRow . data [ secondHeaderKey ] . value &&
169
- keyTextAreaRef . current [ lastRow . id ] . current &&
170
- valueTextAreaRef . current [ lastRow . id ] . current
220
+ ! firstRow . data [ secondHeaderKey ] . value &&
221
+ keyTextAreaRef . current [ firstRow . id ] . current &&
222
+ valueTextAreaRef . current [ firstRow . id ] . current
171
223
) {
172
- keyTextAreaRef . current [ lastRow . id ] . current . focus ( )
224
+ keyTextAreaRef . current [ firstRow . id ] . current . focus ( )
173
225
}
174
226
}
175
227
} , [ newRowAdded ] )
@@ -239,6 +291,7 @@ export const KeyValueTable = <K extends string>({
239
291
240
292
if ( value || row . data [ key === firstHeaderKey ? secondHeaderKey : firstHeaderKey ] . value ) {
241
293
onChange ?.( row . id , key , value )
294
+ onError ?.( ! checkAllRowsAreValid ( updatedRows ) )
242
295
}
243
296
}
244
297
@@ -278,6 +331,30 @@ export const KeyValueTable = <K extends string>({
278
331
</ div >
279
332
)
280
333
334
+ const renderErrorMessage = ( errorMessage : string ) => (
335
+ < div key = { errorMessage } className = "flexbox align-items-center dc__gap-4" >
336
+ < ICClose className = "icon-dim-16 fcr-5 dc__align-self-start dc__no-shrink" />
337
+ < p className = "fs-12 lh-16 cn-7 m-0" > { errorMessage } </ p >
338
+ </ div >
339
+ )
340
+
341
+ const renderErrorMessages = (
342
+ value : Parameters < typeof validationSchema > [ 0 ] ,
343
+ key : Parameters < typeof validationSchema > [ 1 ] ,
344
+ ) => {
345
+ const showErrorMessages = showError && ! validationSchema ( value , key )
346
+ if ( ! showErrorMessages ) {
347
+ return null
348
+ }
349
+
350
+ return (
351
+ < div className = "key-value-table__error bcn-0 dc__border br-4 py-7 px-8 flexbox-col dc__gap-4" >
352
+ { validateDuplicateKeys && renderErrorMessage ( DUPLICATE_KEYS_VALIDATION_MESSAGE ) }
353
+ { parentErrorMessages . map ( ( error ) => renderErrorMessage ( error ) ) }
354
+ </ div >
355
+ )
356
+ }
357
+
281
358
return (
282
359
< >
283
360
< div className = { `bcn-2 p-1 ${ hasRows ? 'dc__top-radius-4' : 'br-4' } ` } >
@@ -314,7 +391,7 @@ export const KeyValueTable = <K extends string>({
314
391
< Fragment key = { key } >
315
392
< ConditionalWrap wrap = { renderWithReadOnlyTippy } condition = { readOnly } >
316
393
< div
317
- className = { `key-value-table__cell bcn-0 flexbox dc__align-items-center dc__gap-4 dc__position-rel ${ readOnly || row . data [ key ] . disabled ? 'cursor-not-allowed no-hover' : '' } ${ showError && ! validationSchema ?. ( row . data [ key ] . value , key ) ? 'key-value-table__cell--error no-hover' : '' } ` }
394
+ className = { `key-value-table__cell bcn-0 flexbox dc__align-items-center dc__gap-4 dc__position-rel ${ readOnly || row . data [ key ] . disabled ? 'cursor-not-allowed no-hover' : '' } ${ showError && ! validationSchema ( row . data [ key ] . value , key ) ? 'key-value-table__cell--error no-hover' : '' } ` }
318
395
>
319
396
{ maskValue ?. [ key ] && row . data [ key ] . value ? (
320
397
< div className = "py-8 px-12 h-36 flex" >
@@ -349,23 +426,7 @@ export const KeyValueTable = <K extends string>({
349
426
*
350
427
</ span >
351
428
) }
352
- { showError &&
353
- ! validationSchema ?.( row . data [ key ] . value , key ) &&
354
- errorMessages . length && (
355
- < div className = "key-value-table__error bcn-0 dc__border br-4 py-7 px-8 flexbox-col dc__gap-4" >
356
- { errorMessages . map ( ( error ) => (
357
- < div
358
- key = { error }
359
- className = "flexbox align-items-center dc__gap-4"
360
- >
361
- < ICClose className = "icon-dim-16 fcr-5 dc__align-self-start dc__no-shrink" />
362
- < p className = "fs-12 lh-16 cn-7 m-0" >
363
- { error }
364
- </ p >
365
- </ div >
366
- ) ) }
367
- </ div >
368
- ) }
429
+ { renderErrorMessages ( row . data [ key ] . value , key ) }
369
430
</ >
370
431
) }
371
432
</ div >
0 commit comments