Skip to content

Commit 29395b4

Browse files
committed
feat: add duplicate validations inside KeyValueTable
1 parent 79eac72 commit 29395b4

File tree

6 files changed

+123
-47
lines changed

6 files changed

+123
-47
lines changed

src/Common/Common.service.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,7 @@ const processCDMaterialsMetaInfo = (cdMaterialsResult): CDMaterialsMetaInfo => {
218218
resourceFilters: cdMaterialsResult.resourceFilters ?? [],
219219
totalCount: cdMaterialsResult.totalCount ?? 0,
220220
requestedUserId: cdMaterialsResult.requestedUserId,
221-
// TODO: Get this casing fixed from API
222-
runtimeParams: parseRuntimeParams(cdMaterialsResult.runtime_params),
221+
runtimeParams: parseRuntimeParams(cdMaterialsResult.runtimeParams),
223222
}
224223
}
225224

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

Lines changed: 97 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { createRef, useEffect, useRef, useState, ReactElement, Fragment } from 'react'
17+
import { createRef, useEffect, useRef, useState, ReactElement, Fragment, useMemo } from 'react'
1818
import Tippy from '@tippyjs/react'
1919
// eslint-disable-next-line import/no-extraneous-dependencies
2020
import { followCursor } from 'tippy.js'
@@ -29,6 +29,7 @@ import { DEFAULT_SECRET_PLACEHOLDER } from '@Shared/constants'
2929

3030
import { KeyValueRow, KeyValueTableProps } from './KeyValueTable.types'
3131
import './KeyValueTable.scss'
32+
import { DUPLICATE_KEYS_VALIDATION_MESSAGE } from './constants'
3233

3334
const renderWithReadOnlyTippy = (children: ReactElement) => (
3435
<Tippy
@@ -54,9 +55,10 @@ export const KeyValueTable = <K extends string>({
5455
isAdditionNotAllowed,
5556
readOnly,
5657
showError,
57-
validationSchema,
58-
errorMessages = [],
58+
validationSchema: parentValidationSchema,
59+
errorMessages: parentErrorMessages = [],
5960
onError,
61+
validateDuplicateKeys = false,
6062
}: KeyValueTableProps<K>) => {
6163
// CONSTANTS
6264
const { headers, rows } = config
@@ -89,11 +91,60 @@ export const KeyValueTable = <K extends string>({
8991
valueTextAreaRef.current = updatedRows.reduce((acc, curr) => ({ ...acc, [curr.id]: createRef() }), {})
9092
}
9193

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(
94145
({ 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),
97148
)
98149

99150
return isValid
@@ -118,7 +169,7 @@ export const KeyValueTable = <K extends string>({
118169

119170
const handleAddNewRow = () => {
120171
const data = getEmptyRow()
121-
const editedRows = [...updatedRows, data]
172+
const editedRows = [data, ...updatedRows]
122173

123174
const { id } = data
124175

@@ -153,23 +204,24 @@ export const KeyValueTable = <K extends string>({
153204
}, [sortOrder])
154205

155206
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) {
158210
setNewRowAdded(false)
159211

160212
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
164216
) {
165-
valueTextAreaRef.current[lastRow.id].current.focus()
217+
valueTextAreaRef.current[firstRow.id].current.focus()
166218
}
167219
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
171223
) {
172-
keyTextAreaRef.current[lastRow.id].current.focus()
224+
keyTextAreaRef.current[firstRow.id].current.focus()
173225
}
174226
}
175227
}, [newRowAdded])
@@ -239,6 +291,7 @@ export const KeyValueTable = <K extends string>({
239291

240292
if (value || row.data[key === firstHeaderKey ? secondHeaderKey : firstHeaderKey].value) {
241293
onChange?.(row.id, key, value)
294+
onError?.(!checkAllRowsAreValid(updatedRows))
242295
}
243296
}
244297

@@ -278,6 +331,30 @@ export const KeyValueTable = <K extends string>({
278331
</div>
279332
)
280333

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+
281358
return (
282359
<>
283360
<div className={`bcn-2 p-1 ${hasRows ? 'dc__top-radius-4' : 'br-4'}`}>
@@ -314,7 +391,7 @@ export const KeyValueTable = <K extends string>({
314391
<Fragment key={key}>
315392
<ConditionalWrap wrap={renderWithReadOnlyTippy} condition={readOnly}>
316393
<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' : ''}`}
318395
>
319396
{maskValue?.[key] && row.data[key].value ? (
320397
<div className="py-8 px-12 h-36 flex">
@@ -349,23 +426,7 @@ export const KeyValueTable = <K extends string>({
349426
*
350427
</span>
351428
)}
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)}
369430
</>
370431
)}
371432
</div>

src/Shared/Components/KeyValueTable/KeyValueTable.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,6 @@
106106
width: 100%;
107107
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
108108
transform: translateX(-50%);
109-
z-index: 1;
109+
z-index: 2;
110110
}
111111
}

src/Shared/Components/KeyValueTable/KeyValueTable.types.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,25 @@ export interface KeyValueConfig<K extends string> {
5555
rows: KeyValueRow<K>[]
5656
}
5757

58+
type ErrorUIProps =
59+
| {
60+
/**
61+
* Indicates whether to show errors.
62+
*/
63+
showError: true
64+
/**
65+
* If true, would show error tooltip on the cell.
66+
*/
67+
validateDuplicateKeys?: boolean
68+
}
69+
| {
70+
/**
71+
* Indicates whether to show errors.
72+
*/
73+
showError?: false
74+
validateDuplicateKeys?: never
75+
}
76+
5877
/**
5978
* Type representing a mask for key-value pairs.
6079
* @template K - A string representing the key type.
@@ -71,7 +90,7 @@ export type KeyValuePlaceholder<K extends string> = {
7190
* Interface representing the properties for a key-value table component.
7291
* @template K - A string representing the key type.
7392
*/
74-
export interface KeyValueTableProps<K extends string> {
93+
export type KeyValueTableProps<K extends string> = {
7594
/** The configuration for the key-value table. */
7695
config: KeyValueConfig<K>
7796
/** An optional mask for the key-value pairs. */
@@ -97,10 +116,6 @@ export interface KeyValueTableProps<K extends string> {
97116
* @param deletedRowIndex - The index of the row that was deleted.
98117
*/
99118
onDelete?: (deletedRowId: string | number) => void
100-
/**
101-
* Indicates whether to show errors.
102-
*/
103-
showError?: boolean
104119
/**
105120
* The function to use to validate the value of the cell.
106121
* @param value - The value to validate.
@@ -118,4 +133,4 @@ export interface KeyValueTableProps<K extends string> {
118133
* @param errorState - The error state, true when any cell has error, otherwise false.
119134
*/
120135
onError?: (errorState: boolean) => void
121-
}
136+
} & ErrorUIProps
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const DUPLICATE_KEYS_VALIDATION_MESSAGE = 'Keys must be unique'

src/Shared/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,6 @@ export interface RuntimeParamsListItemType extends KeyValueListType {
662662
}
663663

664664
export enum RuntimeParamsHeadingType {
665-
KEY = 'Key',
666-
VALUE = 'Value',
665+
KEY = 'key',
666+
VALUE = 'value',
667667
}

0 commit comments

Comments
 (0)