From e17db2526778171254547e81098f5dc73c339b9a Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Tue, 4 Feb 2025 13:11:19 +0200 Subject: [PATCH] Illiar/fix/v2 bugfixes (#3016) * fix: fixed bug when context from tasks didnt get to submit * feat: reworked validation logic * fix: fixed focus on inputs & fixed abortEarlyOnFirstError * fix: adjusted validation params --- .../CollectionFlowUI/CollectionFlowUI.tsx | 10 +- .../inputs/DropdownInput/DropdownInput.tsx | 13 +- .../inputs/MultiSelect/MultiSelect.tsx | 17 +- .../Form/DynamicForm/DynamicForm.tsx | 4 +- .../DynamicForm/DynamicForm.unit.test.tsx | 6 +- .../InputsShowcase/InputsShowcase.tsx | 25 +- .../ValidationShowcase/ValidationShowcase.tsx | 3 +- .../context/dynamic-form.context.ts | 4 +- .../Form/DynamicForm/context/types.ts | 2 +- .../controls/SubmitButton/SubmitButton.tsx | 17 +- .../SubmitButton/SubmitButton.unit.test.tsx | 6 +- .../fields/DocumentField/DocumentField.tsx | 13 +- .../fields/FieldList/FieldList.tsx | 15 +- .../fields/FileField/FileField.tsx | 13 +- .../hooks/external/useField/useField.ts | 13 +- .../external/useField/useField.unit.test.ts | 10 +- .../hooks/external/useSubmit/useSubmit.ts | 15 +- .../external/useSubmit/useSubmit.unit.test.ts | 5 +- .../Form/Validator/ValidatorProvider.tsx | 10 +- .../organisms/Form/Validator/context/types.ts | 2 +- .../internal/useValidate/useAsyncValidate.ts | 46 -- .../useValidate/useAsyncValidate.unit.test.ts | 121 ---- .../internal/useValidate/useManualValidate.ts | 24 - .../useManualValidate.unit.test.ts | 103 --- .../internal/useValidate/useSyncValidate.ts | 23 - .../useValidate/useSyncValidate.unit.test.ts | 108 ---- .../hooks/internal/useValidate/useValidate.ts | 89 ++- .../useValidate/useValidate.unit.test.ts | 606 ++++-------------- .../organisms/Form/Validator/types/index.ts | 4 + .../Validator/utils/validate/exceptions.ts | 5 + .../Form/Validator/utils/validate/types.ts | 1 + .../Form/Validator/utils/validate/validate.ts | 68 +- .../utils/validate/validate.unit.test.ts | 95 ++- .../useRuleEngine/useRuleEngine.unit.test.ts | 2 +- 34 files changed, 454 insertions(+), 1044 deletions(-) delete mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.ts delete mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.ts delete mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.ts delete mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/validate/exceptions.ts diff --git a/apps/kyb-app/src/pages/CollectionFlow/versions/v2/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx b/apps/kyb-app/src/pages/CollectionFlow/versions/v2/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx index 48b29d91b7..d86aaa1857 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/versions/v2/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx +++ b/apps/kyb-app/src/pages/CollectionFlow/versions/v2/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx @@ -3,7 +3,7 @@ import './validator'; import { useDynamicUIContext } from '@/components/organisms/DynamicUI/hooks/useDynamicUIContext'; import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider/hooks/useStateManagerContext'; import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types'; -import { DynamicFormV2, IFormElement, IFormRef } from '@ballerine/ui'; +import { DynamicFormV2, IDynamicFormValidationParams, IFormElement, IFormRef } from '@ballerine/ui'; import { FunctionComponent, useCallback, useMemo, useRef } from 'react'; import { usePluginsSubscribe } from './components/utility/PluginsRunner'; import { usePlugins } from './components/utility/PluginsRunner/hooks/external/usePlugins'; @@ -20,10 +20,12 @@ interface ICollectionFlowUIProps { isRevision?: boolean; } -const validationParams = { - validateOnBlur: false, +const validationParams: IDynamicFormValidationParams = { + validateOnChange: true, + validateOnBlur: true, abortEarly: false, - validationDelay: 500, + abortAfterFirstError: true, + validationDelay: 300, }; export const CollectionFlowUI: FunctionComponent = ({ diff --git a/packages/ui/src/components/molecules/inputs/DropdownInput/DropdownInput.tsx b/packages/ui/src/components/molecules/inputs/DropdownInput/DropdownInput.tsx index 149f7ff6a4..83cd22b728 100644 --- a/packages/ui/src/components/molecules/inputs/DropdownInput/DropdownInput.tsx +++ b/packages/ui/src/components/molecules/inputs/DropdownInput/DropdownInput.tsx @@ -1,7 +1,14 @@ import { CaretSortIcon } from '@radix-ui/react-icons'; import clsx from 'clsx'; import { CheckIcon } from 'lucide-react'; -import React, { FocusEvent, FunctionComponent, useCallback, useMemo, useState } from 'react'; +import React, { + FocusEvent, + FocusEventHandler, + FunctionComponent, + useCallback, + useMemo, + useState, +} from 'react'; import { ctw } from '@/common'; import { @@ -101,6 +108,9 @@ export const DropdownInput: FunctionComponent = ({ props?.trigger?.className, )} disabled={disabled} + tabIndex={0} + onFocus={onFocus as FocusEventHandler} + onBlur={onBlur as FocusEventHandler} data-testid={testId ? `${testId}-trigger` : undefined} > @@ -114,7 +124,6 @@ export const DropdownInput: FunctionComponent = ({ align={props?.content?.align || 'center'} style={{ width: 'var(--radix-popover-trigger-width)' }} className={clsx('p-2', props?.content?.className)} - onBlur={onBlur} > {searchable ? ( diff --git a/packages/ui/src/components/molecules/inputs/MultiSelect/MultiSelect.tsx b/packages/ui/src/components/molecules/inputs/MultiSelect/MultiSelect.tsx index 328a446119..fb94435316 100644 --- a/packages/ui/src/components/molecules/inputs/MultiSelect/MultiSelect.tsx +++ b/packages/ui/src/components/molecules/inputs/MultiSelect/MultiSelect.tsx @@ -5,7 +5,7 @@ import { UnselectButtonProps } from '@/components/molecules/inputs/MultiSelect/c import { SelectedElementParams } from '@/components/molecules/inputs/MultiSelect/types'; import { ClickAwayListener } from '@mui/material'; import keyBy from 'lodash/keyBy'; -import { FocusEvent, useCallback, useMemo, useRef, useState } from 'react'; +import { FocusEvent, FocusEventHandler, useCallback, useMemo, useRef, useState } from 'react'; export type MultiSelectValue = string | number; @@ -50,7 +50,9 @@ export const MultiSelect = ({ const [open, setOpen] = useState(false); const selected = useMemo(() => { - if (!value) return []; + if (!value) { + return []; + } const optionsMap = keyBy(options, 'value'); @@ -124,7 +126,9 @@ export const MultiSelect = ({ }, [options, selected, inputValue]); const handleOutsidePopupClick = useCallback(() => { - if (open) setOpen(false); + if (open) { + setOpen(false); + } }, [open]); const buildUnselectButtonProps = useCallback( @@ -158,7 +162,12 @@ export const MultiSelect = ({ { 'pointer-events-none opacity-50': disabled }, )} > -
+
} + onBlur={onBlur as FocusEventHandler} + > {selected.map(option => { return renderSelected( { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx index 39d82345e7..24e7f8445b 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -39,10 +39,10 @@ export const DynamicFormV2 = forwardRef( }); const touchedApi = useTouched(elements, valuesApi.values); const fieldHelpers = useFieldHelpers({ valuesApi, touchedApi }); - const { submit } = useSubmit({ values: valuesApi.values, onSubmit }); + const { submit } = useSubmit({ onSubmit }); useImperativeHandle(ref, () => ({ - submit, + submit: () => submit(valuesApi.values), validate: () => null, setValues: valuesApi.setValues, setTouched: touchedApi.setTouched, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.unit.test.tsx index 403bb9a0a6..d73915525f 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.unit.test.tsx @@ -150,7 +150,6 @@ describe('DynamicFormV2', () => { it('should pass correct props to useSubmit', () => { render(); expect(useSubmit).toHaveBeenCalledWith({ - values: mockProps.values, onSubmit: mockProps.onSubmit, }); }); @@ -278,11 +277,12 @@ describe('DynamicFormV2', () => { vi.mocked(useFieldHelpers).mockReturnValue(fieldHelpersMock); }); - it('should expose submit method through ref', () => { + it('should call submit method through ref', () => { const ref = { current: null as IFormRef | null }; render(); - expect(ref.current).toHaveProperty('submit', submitMock.submit); + ref.current?.submit(); + expect(submitMock.submit).toHaveBeenCalledWith(valuesMock.values); }); it('should expose validate method through ref', () => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx index 71385c9351..5c1a1c461b 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx @@ -14,7 +14,15 @@ const schema: Array> = [ placeholder: 'Enter text', description: 'This is a text field for entering any text value', }, - validate: [], + validate: [ + { type: 'required', value: {} }, + { + type: 'minLength', + value: { + minLength: 10, + }, + }, + ], }, { id: 'AutocompleteField', @@ -30,6 +38,7 @@ const schema: Array> = [ { value: 'option3', label: 'Option 3' }, ], }, + validate: [{ type: 'required', value: {} }], }, { id: 'CheckboxListField', @@ -44,6 +53,7 @@ const schema: Array> = [ { value: 'option3', label: 'Option 3' }, ], }, + validate: [{ type: 'required', value: {} }], }, { id: 'DateField', @@ -53,6 +63,7 @@ const schema: Array> = [ label: 'Date Field', description: 'Select a date from the calendar', }, + validate: [{ type: 'required', value: {} }], }, { id: 'MultiselectField', @@ -67,6 +78,7 @@ const schema: Array> = [ { value: 'option3', label: 'Option 3' }, ], }, + validate: [{ type: 'required', value: {} }], }, { id: 'SelectField', @@ -81,6 +93,7 @@ const schema: Array> = [ { value: 'option3', label: 'Option 3' }, ], }, + validate: [{ type: 'required', value: {} }], }, { id: 'CheckboxField', @@ -90,6 +103,7 @@ const schema: Array> = [ label: 'Checkbox Field', description: 'Toggle this checkbox for a yes/no selection', }, + validate: [{ type: 'required', value: {} }], }, { id: 'PhoneField', @@ -100,6 +114,7 @@ const schema: Array> = [ description: 'Enter a phone number with country code selection', defaultCountry: 'il', }, + validate: [{ type: 'required', value: {} }], }, { id: 'RadioField', @@ -114,6 +129,7 @@ const schema: Array> = [ { value: 'option3', label: 'Option 3' }, ], }, + validate: [{ type: 'required', value: {} }], }, { id: 'TagsField', @@ -123,6 +139,7 @@ const schema: Array> = [ label: 'Tags Field', description: 'Add multiple tags by typing and pressing enter', }, + validate: [{ type: 'required', value: {} }], }, { id: 'FileField', @@ -133,6 +150,7 @@ const schema: Array> = [ placeholder: 'Select File', description: 'Upload a file from your device', }, + validate: [{ type: 'required', value: {} }], }, { id: 'DocumentField-1', @@ -154,6 +172,7 @@ const schema: Array> = [ resultPath: 'filename', }, }, + validate: [{ type: 'required', value: {} }], }, { id: 'DocumentField-2', @@ -176,6 +195,7 @@ const schema: Array> = [ resultPath: 'filename', }, }, + validate: [{ type: 'required', value: {} }], }, { id: 'FieldList', @@ -185,6 +205,7 @@ const schema: Array> = [ label: 'Field List', description: 'A list of repeatable form fields that can be added or removed', }, + validate: [{ type: 'required', value: {} }], children: [ { id: 'Nested-TextField', @@ -212,6 +233,7 @@ const schema: Array> = [ params: { label: 'Submit Button', }, + validate: [{ type: 'required', value: {} }], }, ]; @@ -228,6 +250,7 @@ export const InputsShowcaseComponent = () => { console.log('onSubmit'); }} onChange={setContext} + validationParams={{ abortAfterFirstError: true }} // onEvent={console.log} />
diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/ValidationShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/ValidationShowcase.tsx index d62e52f60e..94e83e82d0 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/ValidationShowcase.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/ValidationShowcase.tsx @@ -6,7 +6,8 @@ import { IDynamicFormValidationParams } from '../../types'; import { schema } from './schema'; const validationParams: IDynamicFormValidationParams = { - validateOnBlur: false, + validateOnBlur: true, + validateOnChange: false, }; export const ValidationShowcaseComponent = () => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/context/dynamic-form.context.ts b/packages/ui/src/components/organisms/Form/DynamicForm/context/dynamic-form.context.ts index f5617e6457..f46c7508d5 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/context/dynamic-form.context.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/dynamic-form.context.ts @@ -1,4 +1,6 @@ import { createContext } from 'react'; import { IDynamicFormContext } from './types'; -export const DynamicFormContext = createContext({} as IDynamicFormContext); +export const DynamicFormContext = createContext>( + {} as IDynamicFormContext, +); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts b/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts index 724a930e79..2f1c452f67 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts @@ -17,7 +17,7 @@ export interface IDynamicFormContext { touched: ITouchedState; elementsMap: TElementsMap; fieldHelpers: IFieldHelpers; - submit: () => void; + submit: (values: TValues) => void; callbacks: IDynamicFormCallbacks; metadata: Record; validationParams: IDynamicFormValidationParams; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.tsx index a1ee9db4ae..64bd44168a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.tsx @@ -21,15 +21,16 @@ export const SubmitButton: TDynamicFormElement = ({ const { fieldHelpers, values, submit } = useDynamicForm(); const { runTasks, isRunning } = useTaskRunner(); const { sendEvent } = useEvents(element); + const { validate, isValid } = useValidator(); const { touchAllFields } = fieldHelpers; - const { isValid, errors } = useValidator(); - const { disableWhenFormIsInvalid = false, text = 'Submit' } = element.params || {}; const disabled = useMemo(() => { - if (disableWhenFormIsInvalid && !isValid) return true; + if (disableWhenFormIsInvalid && !isValid) { + return true; + } return _disabled; }, [disableWhenFormIsInvalid, isValid, _disabled]); @@ -39,9 +40,12 @@ export const SubmitButton: TDynamicFormElement = ({ touchAllFields(); + const validationResult = await validate(); + const isValid = validationResult?.length === 0; + if (!isValid) { console.log(`Submit button clicked but form is invalid`); - console.log('Validation errors', errors); + console.log('Validation errors', validationResult); return; } @@ -52,10 +56,9 @@ export const SubmitButton: TDynamicFormElement = ({ fieldHelpers.setValues(updatedContext); - submit(); - + submit(updatedContext); sendEvent('onSubmit'); - }, [submit, isValid, touchAllFields, runTasks, sendEvent, errors, onClick, values, fieldHelpers]); + }, [submit, touchAllFields, runTasks, sendEvent, onClick, values, fieldHelpers, validate]); return (