From 75d4a34ad39073c5bb39e4b42f97aef410888b3b Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Mon, 2 Dec 2024 16:47:32 +0200 Subject: [PATCH 01/54] feat: added initial validator boilerplate & utilities & tests --- packages/ui/package.json | 3 + .../src/components/organisms/Form/.gitignore | 1 + .../Form/Validator/ValidatorProvider.tsx | 23 + .../organisms/Form/Validator/context/index.ts | 2 + .../organisms/Form/Validator/context/types.ts | 8 + .../Validator/context/validator-context.ts | 11 + .../hooks/external/useValidator/index.ts | 1 + .../external/useValidator/useValidator.ts | 12 + .../hooks/internal/useValidatorRef/index.ts | 2 + .../hooks/internal/useValidatorRef/types.ts | 3 + .../useValidatorRef/useValidatorRef.ts | 13 + .../useValidatorRef.unit.test.ts | 45 + .../organisms/Form/Validator/index.ts | 4 + .../organisms/Form/Validator/types/index.ts | 41 + .../format-error-message.ts | 2 + .../format-error-message.unit.test.ts | 34 + .../utils/format-error-message/index.ts | 1 + .../utils/get-validator/get-validator.ts | 12 + .../get-validator/get-validator.unit.test.ts | 40 + .../Validator/utils/get-validator/index.ts | 1 + .../utils/register-validator/index.ts | 1 + .../register-validator/register-validator.ts | 8 + .../register-validator.unit.test.ts | 28 + .../Form/Validator/utils/validate/index.ts | 1 + .../Form/Validator/utils/validate/validate.ts | 0 .../validators/format/format-validator.ts | 23 + .../format/format.validator.unit.test.ts | 51 + .../Form/Validator/validators/format/index.ts | 2 + .../Form/Validator/validators/format/types.ts | 3 + .../Form/Validator/validators/index.ts | 18 + .../Validator/validators/max-length/index.ts | 2 + .../max-length/max-length-validator.ts | 14 + .../max-length.validator.unit.test.ts | 44 + .../Validator/validators/max-length/types.ts | 3 + .../Validator/validators/maximum/index.ts | 2 + .../validators/maximum/maximum-validator.ts | 14 + .../maximum/maximum.validator.unit.test.ts | 41 + .../Validator/validators/maximum/types.ts | 3 + .../Validator/validators/min-length/index.ts | 2 + .../min-length/min-length-validator.ts | 14 + .../min-length.validator.unit.test.ts | 46 + .../Validator/validators/min-length/types.ts | 3 + .../Validator/validators/minimum/index.ts | 2 + .../minimum/minimum-value-validator.ts | 14 + .../minimum/minimum.validator.unit.test.ts | 34 + .../Validator/validators/minimum/types.ts | 3 + .../Validator/validators/pattern/index.ts | 2 + .../validators/pattern/pattern-validator.ts | 14 + .../pattern/pattern.validator.unit.test.ts | 42 + .../Validator/validators/pattern/types.ts | 3 + .../Validator/validators/required/index.ts | 0 .../validators/required/required-validator.ts | 13 + .../required/required.validator.unit.test.ts | 46 + .../Validator/validators/required/types.ts | 3 + .../organisms/Form/_Validator/Validator.tsx | 41 + .../_Validator/hooks/useValidate/index.ts | 3 + .../hooks/useValidate/ui-element.ts | 79 + .../hooks/useValidate/useValidate.ts | 24 + ...lue-destination-and-apply-stack-indexes.ts | 10 + .../_Validator/hooks/useValidate/validate.ts | 98 + .../hooks/useValidatedInput/index.ts | 1 + .../useValidatedInput/useValidatedInput.ts | 8 + .../_Validator/hooks/useValidator/index.ts | 1 + .../hooks/useValidator/useValidator.ts | 4 + .../organisms/Form/_Validator/index.ts | 1 + .../organisms/Form/_Validator/types.ts | 63 + .../Form/_Validator/validator.context.ts | 12 + .../_Validator/value-validator-manager.ts | 37 + .../document.value.validator.ts | 42 + .../document.value.validator.unit.test.ts | 186 ++ .../format.value.validator.ts | 29 + .../format.value.validator.unit.test.ts | 58 + .../max-length.value.validator.ts | 26 + .../max-length.value.validator.unit.test.ts | 63 + .../maximum.value.validator.ts | 23 + .../maximum.value.validator.unit.test.ts | 63 + .../min-length.value.validator.ts | 26 + .../min-length.value.validator.unit.test.ts | 63 + .../minimum.value.validator.ts | 23 + .../minimum.value.validator.unit.test.ts | 63 + .../pattern.value.validator.ts | 23 + .../pattern.value.validator.unit.test.ts | 54 + .../required.value-validator.ts | 22 + .../required.value-validator.unit.test.ts | 57 + .../value-validator.abstract.ts | 9 + packages/ui/src/setupTests.ts | 5 + packages/ui/vite.config.ts | 3 +- pnpm-lock.yaml | 1963 ++++++++++++++++- 88 files changed, 3820 insertions(+), 121 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/.gitignore create mode 100644 packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx create mode 100644 packages/ui/src/components/organisms/Form/Validator/context/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/context/types.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/context/validator-context.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/external/useValidator/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/external/useValidator/useValidator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/types.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/types/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/format-error-message.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/format-error-message.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/get-validator/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/register-validator/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/register-validator/register-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/register-validator/register-validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/validate/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/format/format-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/format/format.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/format/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/format/types.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/max-length/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/max-length/types.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/maximum/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/maximum/types.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/min-length/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/min-length/types.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/minimum/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum-value-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/minimum/types.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/pattern/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/pattern/types.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/required/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/required/required-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/required/required.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/validators/required/types.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/Validator.tsx create mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/index.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/ui-element.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/useValidate.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/validate.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/index.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/useValidatedInput.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/index.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/useValidator.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/index.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/types.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/validator.context.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validator-manager.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/value-validator.abstract.ts create mode 100644 packages/ui/src/setupTests.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index fcd87d42c2..8aa5a22c86 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -51,6 +51,7 @@ "clsx": "^1.2.1", "cmdk": "^0.2.0", "dayjs": "^1.11.6", + "email-validator": "^2.0.4", "i18n-iso-countries": "^7.6.0", "lodash": "^4.17.21", "lucide-react": "^0.144.0", @@ -76,6 +77,8 @@ "@storybook/react": "^7.0.26", "@storybook/react-vite": "^7.0.26", "@storybook/testing-library": "^0.0.14-next.2", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^13.3.0", "@types/lodash": "^4.14.191", "@types/node": "^20.4.1", "@types/react": "^18.0.37", diff --git a/packages/ui/src/components/organisms/Form/.gitignore b/packages/ui/src/components/organisms/Form/.gitignore new file mode 100644 index 0000000000..1d7655327c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/.gitignore @@ -0,0 +1 @@ +./_Validator diff --git a/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx b/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx new file mode 100644 index 0000000000..3d78aceb8a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx @@ -0,0 +1,23 @@ +import { ValidatorContext } from './context'; +import { IValidatorRef, useValidatorRef } from './hooks/internal/useValidatorRef'; +import { IValidationSchema } from './types'; + +export interface IValidatorProviderProps { + children: React.ReactNode | React.ReactNode[]; + schema: IValidationSchema[]; + value: TValue; + + ref?: React.RefObject; + validateOnChange?: boolean; +} + +export const ValidatorProvider = ({ + children, + schema, + value, + ref, +}: IValidatorProviderProps) => { + useValidatorRef(ref); + + return {children}; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/context/index.ts b/packages/ui/src/components/organisms/Form/Validator/context/index.ts new file mode 100644 index 0000000000..ae9c667c47 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/context/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './validator-context'; diff --git a/packages/ui/src/components/organisms/Form/Validator/context/types.ts b/packages/ui/src/components/organisms/Form/Validator/context/types.ts new file mode 100644 index 0000000000..f6fe1a939c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/context/types.ts @@ -0,0 +1,8 @@ +import { IValidationError } from '../types'; + +export interface IValidatorContext { + errors: IValidationError[]; + values: TValues; + isValid: boolean; + validate: () => void; +} diff --git a/packages/ui/src/components/organisms/Form/Validator/context/validator-context.ts b/packages/ui/src/components/organisms/Form/Validator/context/validator-context.ts new file mode 100644 index 0000000000..019bee129d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/context/validator-context.ts @@ -0,0 +1,11 @@ +import { createContext } from 'react'; +import { IValidatorContext } from './types'; + +export const ValidatorContext = createContext>({ + errors: [], + values: {}, + isValid: true, + validate: () => { + throw new Error('Validator context is not provided.'); + }, +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/external/useValidator/index.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/external/useValidator/index.ts new file mode 100644 index 0000000000..df0ef89dfd --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/external/useValidator/index.ts @@ -0,0 +1 @@ +export * from './useValidator'; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/external/useValidator/useValidator.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/external/useValidator/useValidator.ts new file mode 100644 index 0000000000..74390b87a1 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/external/useValidator/useValidator.ts @@ -0,0 +1,12 @@ +import { useContext } from 'react'; +import { ValidatorContext } from '../../../context'; + +export const useValidator = () => { + const context = useContext(ValidatorContext); + + if (!context) { + throw new Error('Validator context is not provided.'); + } + + return context; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/index.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/index.ts new file mode 100644 index 0000000000..f7d95428e2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './useValidatorRef'; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/types.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/types.ts new file mode 100644 index 0000000000..65ed5afcdd --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/types.ts @@ -0,0 +1,3 @@ +export interface IValidatorRef { + validate: () => void; +} diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.ts new file mode 100644 index 0000000000..5481715277 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.ts @@ -0,0 +1,13 @@ +import { useImperativeHandle } from 'react'; +import { useValidator } from '../../external/useValidator/useValidator'; +import { IValidatorRef } from './types'; + +export const useValidatorRef = (refObject?: React.RefObject): IValidatorRef => { + const context = useValidator(); + + useImperativeHandle(refObject, () => ({ + validate: context.validate, + })); + + return context; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.unit.test.ts new file mode 100644 index 0000000000..7dc39283ba --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.unit.test.ts @@ -0,0 +1,45 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useValidator } from '../../external/useValidator/useValidator'; +import { useValidatorRef } from './useValidatorRef'; + +const mockValidate = vi.fn(); + +vi.mock('../../external/useValidator/useValidator', () => ({ + useValidator: vi.fn(() => ({ + validate: mockValidate, + })), +})); + +describe('useValidatorRef', () => { + const mockRef = { current: null }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return context from useValidator', () => { + const { result } = renderHook(() => useValidatorRef()); + + expect(result.current).toEqual({ + validate: mockValidate, + }); + expect(useValidator).toHaveBeenCalled(); + }); + + it('should set ref.current.validate to context.validate when ref is provided', () => { + renderHook(() => useValidatorRef(mockRef)); + + expect(mockRef.current).toEqual({ + validate: mockValidate, + }); + }); + + it('should not set ref when no ref object is provided', () => { + const { result } = renderHook(() => useValidatorRef()); + + expect(result.current).toEqual({ + validate: mockValidate, + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/index.ts b/packages/ui/src/components/organisms/Form/Validator/index.ts new file mode 100644 index 0000000000..392efee948 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/index.ts @@ -0,0 +1,4 @@ +export * from './hooks/external/useValidator'; +export * from './types'; +export * from './utils/register-validator'; +export * from './ValidatorProvider'; diff --git a/packages/ui/src/components/organisms/Form/Validator/types/index.ts b/packages/ui/src/components/organisms/Form/Validator/types/index.ts new file mode 100644 index 0000000000..3525118cbd --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/types/index.ts @@ -0,0 +1,41 @@ +export type TBaseValidationRules = 'json-logic'; + +export interface IValidationRule { + type: TBaseValidationRules; + value: object; +} + +export type TBaseValidators = + | 'required' + | 'minLength' + | 'maxLength' + | 'pattern' + | 'minimum' + | 'maximum'; + +export interface ICommonValidator { + type: TValidatorType; + value: T; + message?: string; + applyWhen?: IValidationRule; +} + +export interface IValidationSchema { + id: string; + validators: ICommonValidator[]; + children?: IValidationSchema[]; +} + +export interface IValidationError { + id: string; + originId: string; + invalidValue: unknown; + message: string[]; +} + +export * from '../hooks/internal/useValidatorRef/types'; + +export type TValidator = ( + value: T, + validator: ICommonValidator, +) => void; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/format-error-message.ts b/packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/format-error-message.ts new file mode 100644 index 0000000000..d445f03ec2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/format-error-message.ts @@ -0,0 +1,2 @@ +export const formatErrorMessage = (message: string, key: string, value: string) => + message.replaceAll(`{${key}}`, value); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/format-error-message.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/format-error-message.unit.test.ts new file mode 100644 index 0000000000..ed29c1498f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/format-error-message.unit.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { formatErrorMessage } from './format-error-message'; + +describe('formatErrorMessage', () => { + it('should replace single placeholder with value', () => { + const message = 'This is a {test} message'; + const result = formatErrorMessage(message, 'test', 'sample'); + expect(result).toBe('This is a sample message'); + }); + + it('should replace multiple occurrences of the same placeholder', () => { + const message = 'The {value} is equal to {value}'; + const result = formatErrorMessage(message, 'value', '42'); + expect(result).toBe('The 42 is equal to 42'); + }); + + it('should not modify message when placeholder is not found', () => { + const message = 'This message has no placeholders'; + const result = formatErrorMessage(message, 'key', 'value'); + expect(result).toBe('This message has no placeholders'); + }); + + it('should handle empty strings', () => { + const message = ''; + const result = formatErrorMessage(message, 'key', 'value'); + expect(result).toBe(''); + }); + + it('should handle special characters in placeholder values', () => { + const message = 'Special {char} test'; + const result = formatErrorMessage(message, 'char', '$@#'); + expect(result).toBe('Special $@# test'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/index.ts b/packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/index.ts new file mode 100644 index 0000000000..7a3ed8a2fa --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/format-error-message/index.ts @@ -0,0 +1 @@ +export * from './format-error-message'; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.ts b/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.ts new file mode 100644 index 0000000000..5ce8a0d309 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.ts @@ -0,0 +1,12 @@ +import { ICommonValidator } from '../../types'; +import { baseValidatorsMap, validatorsExtends } from '../../validators'; + +export const getValidator = (validator: ICommonValidator) => { + const validatorFn = baseValidatorsMap[validator.type] || validatorsExtends[validator.type]; + + if (!validatorFn) { + throw new Error(`Validator ${validator.type} not found.`); + } + + return validatorFn; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.unit.test.ts new file mode 100644 index 0000000000..d11c0f4eea --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.unit.test.ts @@ -0,0 +1,40 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ICommonValidator, TBaseValidators } from '../../types'; +import { baseValidatorsMap, validatorsExtends } from '../../validators'; +import { getValidator } from './get-validator'; + +vi.mock('../../validators', () => ({ + baseValidatorsMap: {}, + validatorsExtends: {}, +})); + +describe('getValidator', () => { + const mockValidator = vi.fn(); + const validatorType = 'test'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return validator from baseValidatorsMap if exists', () => { + baseValidatorsMap[validatorType as TBaseValidators] = mockValidator; + + const result = getValidator({ type: validatorType } as unknown as ICommonValidator); + + expect(result).toBe(mockValidator); + }); + + it('should return validator from validatorsExtends if exists and not in baseValidatorsMap', () => { + validatorsExtends[validatorType] = mockValidator; + + const result = getValidator({ type: validatorType } as unknown as ICommonValidator); + + expect(result).toBe(mockValidator); + }); + + it('should throw error if validator not found', () => { + expect(() => + getValidator({ type: 'nonexistent' as TBaseValidators } as unknown as ICommonValidator), + ).toThrow('Validator nonexistent not found.'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/index.ts b/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/index.ts new file mode 100644 index 0000000000..364c7cbd05 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/index.ts @@ -0,0 +1 @@ +export * from './get-validator'; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/register-validator/index.ts b/packages/ui/src/components/organisms/Form/Validator/utils/register-validator/index.ts new file mode 100644 index 0000000000..53eac2b360 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/register-validator/index.ts @@ -0,0 +1 @@ +export * from './register-validator'; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/register-validator/register-validator.ts b/packages/ui/src/components/organisms/Form/Validator/utils/register-validator/register-validator.ts new file mode 100644 index 0000000000..56060bf353 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/register-validator/register-validator.ts @@ -0,0 +1,8 @@ +import { TValidator } from '../../types'; +import { validatorsExtends } from '../../validators'; + +export const registerValidator = (type: string, validator: TValidator) => { + validatorsExtends[type] = validator; + + return validator; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/register-validator/register-validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/register-validator/register-validator.unit.test.ts new file mode 100644 index 0000000000..67b2e62595 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/register-validator/register-validator.unit.test.ts @@ -0,0 +1,28 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { validatorsExtends } from '../../validators'; +import { registerValidator } from './register-validator'; + +vi.mock('../../validators', () => ({ + validatorsExtends: {}, +})); + +describe('registerValidator', () => { + const mockValidator = vi.fn(); + const validatorType = 'test'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should register validator to validatorsExtends', () => { + registerValidator(validatorType, mockValidator); + + expect(validatorsExtends[validatorType]).toBe(mockValidator); + }); + + it('should return the registered validator', () => { + const result = registerValidator(validatorType, mockValidator); + + expect(result).toBe(mockValidator); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/validate/index.ts b/packages/ui/src/components/organisms/Form/Validator/utils/validate/index.ts new file mode 100644 index 0000000000..c1e396d956 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/validate/index.ts @@ -0,0 +1 @@ +export * from './validate'; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/format/format-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/format/format-validator.ts new file mode 100644 index 0000000000..f0a34274a9 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/format/format-validator.ts @@ -0,0 +1,23 @@ +import EmailValidator from 'email-validator'; +import { TValidator } from '../../types'; +import { formatErrorMessage } from '../../utils/format-error-message'; +import { IFormatValueValidatorParams } from './types'; + +export const formatValidator: TValidator = ( + value, + params, +) => { + const { message = 'Invalid {format} format.' } = params; + + if (params.value.format === 'email') { + const isValid = EmailValidator.validate(value as string); + + if (!isValid) { + throw new Error(formatErrorMessage(message, 'format', 'email')); + } + + return true; + } + + throw new Error(`Format validator ${params.value.format} is not supported.`); +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/format/format.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/format/format.validator.unit.test.ts new file mode 100644 index 0000000000..9c083d3983 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/format/format.validator.unit.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest'; +import { ICommonValidator } from '../../types'; +import { formatValidator } from './format-validator'; + +describe('formatValidator', () => { + describe('email format', () => { + const params = { + value: { format: 'email' }, + }; + + it('should not throw error for valid email', () => { + expect(() => + formatValidator('test@example.com', params as ICommonValidator), + ).not.toThrow(); + }); + + it('should return true for valid email', () => { + expect(formatValidator('test@example.com', params as ICommonValidator)).toBe(true); + }); + + it('should throw error for invalid email', () => { + expect(() => formatValidator('invalid-email', params as ICommonValidator)).toThrow( + 'Invalid email format.', + ); + }); + + it('should throw error for empty string', () => { + expect(() => formatValidator('', params as ICommonValidator)).toThrow( + 'Invalid email format.', + ); + }); + + it('should throw error for non-string value', () => { + expect(() => formatValidator(123, params as ICommonValidator)).toThrow( + 'Invalid email format.', + ); + }); + }); + + describe('unsupported format', () => { + const params = { + value: { format: 'phone' }, + }; + + it('should throw error for unsupported format', () => { + expect(() => formatValidator('123-456-7890', params as ICommonValidator)).toThrow( + 'Format validator phone is not supported.', + ); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/format/index.ts b/packages/ui/src/components/organisms/Form/Validator/validators/format/index.ts new file mode 100644 index 0000000000..42eb3a4dea --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/format/index.ts @@ -0,0 +1,2 @@ +export * from './format-validator'; +export * from './types'; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/format/types.ts b/packages/ui/src/components/organisms/Form/Validator/validators/format/types.ts new file mode 100644 index 0000000000..8202ed8290 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/format/types.ts @@ -0,0 +1,3 @@ +export interface IFormatValueValidatorParams { + format: 'email'; +} diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/index.ts b/packages/ui/src/components/organisms/Form/Validator/validators/index.ts new file mode 100644 index 0000000000..b62c5a1ec7 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/index.ts @@ -0,0 +1,18 @@ +import { TBaseValidators, TValidator } from '../types'; +import { maxLengthValidator } from './max-length'; +import { maximumValueValidator } from './maximum'; +import { minLengthValidator } from './min-length'; +import { minimumValueValidator } from './minimum'; +import { patternValueValidator } from './pattern'; +import { requiredValueValidator } from './required/required-validator'; + +export const baseValidatorsMap: Record> = { + required: requiredValueValidator, + minLength: minLengthValidator, + maxLength: maxLengthValidator, + pattern: patternValueValidator, + minimum: minimumValueValidator, + maximum: maximumValueValidator, +}; + +export const validatorsExtends: Record> = {}; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/max-length/index.ts b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/index.ts new file mode 100644 index 0000000000..e5bc153c1f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/index.ts @@ -0,0 +1,2 @@ +export * from './max-length-validator'; +export * from './types'; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length-validator.ts new file mode 100644 index 0000000000..824566ba61 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length-validator.ts @@ -0,0 +1,14 @@ +import { TValidator } from '../../types'; +import { formatErrorMessage } from '../../utils/format-error-message'; +import { IMaxLengthValueValidatorParams } from './types'; + +export const maxLengthValidator: TValidator = ( + value, + params, +) => { + const { message = 'Maximum length is {maxLength}.' } = params; + + if (value?.length > params.value.maxLength) { + throw new Error(formatErrorMessage(message, 'maxLength', params.value.maxLength.toString())); + } +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length.validator.unit.test.ts new file mode 100644 index 0000000000..3c6880d4ac --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length.validator.unit.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { ICommonValidator } from '../../types'; +import { maxLengthValidator } from './max-length-validator'; + +describe('maxLengthValidator', () => { + const params = { + value: { maxLength: 5 }, + }; + + it('should not throw error when string length is equal to maxLength', () => { + expect(() => maxLengthValidator('12345', params as ICommonValidator)).not.toThrow(); + }); + + it('should not throw error when string length is less than maxLength', () => { + expect(() => maxLengthValidator('1234', params as ICommonValidator)).not.toThrow(); + }); + + it('should throw error when string length exceeds maxLength', () => { + expect(() => maxLengthValidator('123456', params as ICommonValidator)).toThrow( + 'Maximum length is 5.', + ); + }); + + it('should handle custom error message', () => { + const customParams = { + value: { maxLength: 5 }, + message: 'Text cannot be longer than {maxLength} characters', + }; + + expect(() => maxLengthValidator('123456', customParams as ICommonValidator)).toThrow( + 'Text cannot be longer than 5 characters', + ); + }); + + it('should handle empty string', () => { + expect(() => maxLengthValidator('', params as ICommonValidator)).not.toThrow(); + }); + + it('should handle undefined value', () => { + expect(() => + maxLengthValidator(undefined as any, params as ICommonValidator), + ).not.toThrow(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/max-length/types.ts b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/types.ts new file mode 100644 index 0000000000..c1c7b18d52 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/types.ts @@ -0,0 +1,3 @@ +export interface IMaxLengthValueValidatorParams { + maxLength: number; +} diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/maximum/index.ts b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/index.ts new file mode 100644 index 0000000000..a11fc4d6a2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/index.ts @@ -0,0 +1,2 @@ +export * from './maximum-validator'; +export * from './types'; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum-validator.ts new file mode 100644 index 0000000000..31b1716b59 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum-validator.ts @@ -0,0 +1,14 @@ +import { TValidator } from '../../types'; +import { formatErrorMessage } from '../../utils/format-error-message'; +import { IMaximumValueValidatorParams } from './types'; + +export const maximumValueValidator: TValidator = ( + value, + params, +) => { + const { message = 'Maximum value is {maximum}.' } = params; + + if (value > params.value.maximum) { + throw new Error(formatErrorMessage(message, 'maximum', params.value.maximum.toString())); + } +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum.validator.unit.test.ts new file mode 100644 index 0000000000..771c421eab --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum.validator.unit.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; +import { ICommonValidator } from '../../types'; +import { maximumValueValidator } from './maximum-validator'; + +describe('maximumValueValidator', () => { + const params = { + value: { maximum: 10 }, + }; + + it('should not throw error when value is equal to maximum', () => { + expect(() => maximumValueValidator(10, params as ICommonValidator)).not.toThrow(); + }); + + it('should not throw error when value is less than maximum', () => { + expect(() => maximumValueValidator(9, params as ICommonValidator)).not.toThrow(); + }); + + it('should throw error when value exceeds maximum', () => { + expect(() => maximumValueValidator(11, params as ICommonValidator)).toThrow( + 'Maximum value is 10.', + ); + }); + + it('should handle custom error message', () => { + const customParams = { + value: { maximum: 10 }, + message: 'Value cannot be greater than {maximum}', + }; + + expect(() => maximumValueValidator(11, customParams as ICommonValidator)).toThrow( + 'Value cannot be greater than 10', + ); + }); + + it('should handle decimal values', () => { + expect(() => maximumValueValidator(10.1, params as ICommonValidator)).toThrow( + 'Maximum value is 10.', + ); + expect(() => maximumValueValidator(9.9, params as ICommonValidator)).not.toThrow(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/maximum/types.ts b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/types.ts new file mode 100644 index 0000000000..51b528a042 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/types.ts @@ -0,0 +1,3 @@ +export interface IMaximumValueValidatorParams { + maximum: number; +} diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/min-length/index.ts b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/index.ts new file mode 100644 index 0000000000..ed38324db5 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/index.ts @@ -0,0 +1,2 @@ +export * from './min-length-validator'; +export * from './types'; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length-validator.ts new file mode 100644 index 0000000000..5749e4c004 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length-validator.ts @@ -0,0 +1,14 @@ +import { TValidator } from '../../types'; +import { formatErrorMessage } from '../../utils/format-error-message'; +import { IMinLengthValueValidatorParams } from './types'; + +export const minLengthValidator: TValidator = ( + value, + params, +) => { + const { message = 'Minimum length is {minLength}.' } = params; + + if (value === undefined || value?.length < params.value.minLength) { + throw new Error(formatErrorMessage(message, 'minLength', params.value.minLength.toString())); + } +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length.validator.unit.test.ts new file mode 100644 index 0000000000..25d7920637 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length.validator.unit.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { ICommonValidator } from '../../types'; +import { minLengthValidator } from './min-length-validator'; + +describe('minLengthValidator', () => { + const params = { + value: { minLength: 4 }, + }; + + it('should not throw error when string length is equal to minLength', () => { + expect(() => minLengthValidator('test', params as ICommonValidator)).not.toThrow(); + }); + + it('should not throw error when string length is greater than minLength', () => { + expect(() => minLengthValidator('testing', params as ICommonValidator)).not.toThrow(); + }); + + it('should throw error when string length is less than minLength', () => { + expect(() => minLengthValidator('te', params as ICommonValidator)).toThrow( + 'Minimum length is 4.', + ); + }); + + it('should handle custom error message', () => { + const customParams = { + value: { minLength: 4 }, + message: 'Custom message: {minLength}', + }; + + expect(() => minLengthValidator('te', customParams as ICommonValidator)).toThrow( + 'Custom message: 4', + ); + }); + + it('should handle empty string', () => { + expect(() => minLengthValidator('', params as ICommonValidator)).toThrow( + 'Minimum length is 4.', + ); + }); + + it('should handle undefined value', () => { + expect(() => minLengthValidator(undefined as any, params as ICommonValidator)).toThrow( + 'Minimum length is 4.', + ); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/min-length/types.ts b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/types.ts new file mode 100644 index 0000000000..216856bf9d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/types.ts @@ -0,0 +1,3 @@ +export interface IMinLengthValueValidatorParams { + minLength: number; +} diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/minimum/index.ts b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/index.ts new file mode 100644 index 0000000000..93f31905da --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/index.ts @@ -0,0 +1,2 @@ +export * from './minimum-value-validator'; +export * from './types'; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum-value-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum-value-validator.ts new file mode 100644 index 0000000000..e33747bdef --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum-value-validator.ts @@ -0,0 +1,14 @@ +import { TValidator } from '../../types'; +import { formatErrorMessage } from '../../utils/format-error-message'; +import { IMinimumValueValidatorParams } from './types'; + +export const minimumValueValidator: TValidator = ( + value, + params, +) => { + const { message = 'Minimum value is {minimum}.' } = params; + + if (value < params.value.minimum) { + throw new Error(formatErrorMessage(message, 'minimum', params.value.minimum.toString())); + } +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum.validator.unit.test.ts new file mode 100644 index 0000000000..4aad13ac10 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum.validator.unit.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { ICommonValidator } from '../../types'; +import { minimumValueValidator } from './minimum-value-validator'; + +describe('minimumValueValidator', () => { + const params = { + value: { minimum: 5 }, + }; + + it('should not throw error when value is equal to minimum', () => { + expect(() => minimumValueValidator(5, params as ICommonValidator)).not.toThrow(); + }); + + it('should not throw error when value is greater than minimum', () => { + expect(() => minimumValueValidator(10, params as ICommonValidator)).not.toThrow(); + }); + + it('should throw error when value is less than minimum', () => { + expect(() => minimumValueValidator(3, params as ICommonValidator)).toThrow( + 'Minimum value is 5.', + ); + }); + + it('should handle custom error message', () => { + const customParams = { + value: { minimum: 5 }, + message: 'Custom message: min {minimum}', + }; + + expect(() => minimumValueValidator(3, customParams as ICommonValidator)).toThrow( + 'Custom message: min 5', + ); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/minimum/types.ts b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/types.ts new file mode 100644 index 0000000000..531b751158 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/types.ts @@ -0,0 +1,3 @@ +export interface IMinimumValueValidatorParams { + minimum: number; +} diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/pattern/index.ts b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/index.ts new file mode 100644 index 0000000000..04375ebeb0 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/index.ts @@ -0,0 +1,2 @@ +export * from './pattern-validator'; +export * from './types'; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern-validator.ts new file mode 100644 index 0000000000..19c46ec713 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern-validator.ts @@ -0,0 +1,14 @@ +import { TValidator } from '../../types'; +import { formatErrorMessage } from '../../utils/format-error-message'; +import { IPatternValidatorParams } from './types'; + +export const patternValueValidator: TValidator = ( + value, + params, +) => { + const { message = `Value must match {pattern}.` } = params; + + if (!new RegExp(params.value.pattern).test(value as string)) { + throw new Error(formatErrorMessage(message, 'pattern', params.value.pattern)); + } +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern.validator.unit.test.ts new file mode 100644 index 0000000000..643e122919 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern.validator.unit.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { ICommonValidator } from '../../types'; +import { patternValueValidator } from './pattern-validator'; + +describe('patternValueValidator', () => { + const params = { + value: { pattern: '^[A-Z]+$' }, + }; + + it('should not throw error when value matches pattern', () => { + expect(() => patternValueValidator('ABC', params as ICommonValidator)).not.toThrow(); + }); + + it('should throw error when value does not match pattern', () => { + expect(() => patternValueValidator('abc', params as ICommonValidator)).toThrow( + 'Value must match ^[A-Z]+$.', + ); + }); + + it('should handle custom error message', () => { + const customParams = { + value: { pattern: '^[A-Z]+$' }, + message: 'Custom message: {pattern}', + }; + + expect(() => patternValueValidator('abc', customParams as ICommonValidator)).toThrow( + 'Custom message: ^[A-Z]+$', + ); + }); + + it('should handle empty string', () => { + expect(() => patternValueValidator('', params as ICommonValidator)).toThrow( + 'Value must match ^[A-Z]+$.', + ); + }); + + it('should handle undefined value', () => { + expect(() => patternValueValidator(undefined as any, params as ICommonValidator)).toThrow( + 'Value must match ^[A-Z]+$.', + ); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/pattern/types.ts b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/types.ts new file mode 100644 index 0000000000..4db54ae051 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/types.ts @@ -0,0 +1,3 @@ +export interface IPatternValidatorParams { + pattern: string; +} diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/required/index.ts b/packages/ui/src/components/organisms/Form/Validator/validators/required/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/required/required-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/required/required-validator.ts new file mode 100644 index 0000000000..9b42bce771 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/required/required-validator.ts @@ -0,0 +1,13 @@ +import { TValidator } from '../../types'; +import { IRequiredValueValidatorParams } from './types'; + +export const requiredValueValidator: TValidator = ( + value, + params, +) => { + const { message = 'Required value.' } = params; + + if (value === undefined || value === null || value === '') { + throw new Error(message); + } +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/required/required.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/required/required.validator.unit.test.ts new file mode 100644 index 0000000000..07a6803a24 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/required/required.validator.unit.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { ICommonValidator } from '../../types'; +import { requiredValueValidator } from './required-validator'; + +describe('requiredValueValidator', () => { + const params = { + value: { required: true }, + }; + + it('should not throw error when value is provided', () => { + expect(() => requiredValueValidator('test', params as ICommonValidator)).not.toThrow(); + }); + + it('should not throw error when value is zero', () => { + expect(() => requiredValueValidator(0, params as ICommonValidator)).not.toThrow(); + }); + + it('should throw error when value is undefined', () => { + expect(() => requiredValueValidator(undefined, params as ICommonValidator)).toThrow( + 'Required value.', + ); + }); + + it('should throw error when value is null', () => { + expect(() => requiredValueValidator(null, params as ICommonValidator)).toThrow( + 'Required value.', + ); + }); + + it('should throw error when value is empty string', () => { + expect(() => requiredValueValidator('', params as ICommonValidator)).toThrow( + 'Required value.', + ); + }); + + it('should handle custom error message', () => { + const customParams = { + value: { required: true }, + message: 'Custom required message', + }; + + expect(() => requiredValueValidator('', customParams as ICommonValidator)).toThrow( + 'Custom required message', + ); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/required/types.ts b/packages/ui/src/components/organisms/Form/Validator/validators/required/types.ts new file mode 100644 index 0000000000..6d87c9b48e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/validators/required/types.ts @@ -0,0 +1,3 @@ +export interface IRequiredValueValidatorParams { + required: boolean; +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/Validator.tsx b/packages/ui/src/components/organisms/Form/_Validator/Validator.tsx new file mode 100644 index 0000000000..16b073fc29 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/Validator.tsx @@ -0,0 +1,41 @@ +import { useValidate } from '@/components/providers/Validator/hooks/useValidate'; +import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; +import { UIElementV2 } from '@/components/providers/Validator/types'; +import React, { FunctionComponent, useCallback, useMemo, useState } from 'react'; +import { TValidationErrors, validatorContext } from './validator.context'; + +const { Provider } = validatorContext; + +export interface IValidatorProps { + children: React.ReactNode | React.ReactNode[]; + context: unknown; + elements: UIElementV2[]; +} + +export const Validator: FunctionComponent = ({ children, elements, context }) => { + const validate = useValidate({ elements, context }); + const [validationErrors, setValiationErrors] = useState({}); + + console.log({ validationErrors }); + + const onValidate = useCallback(() => { + const errors = validate(); + const validationErrors = errors.reduce((acc, error) => { + const element = new UIElement(error.element, context, error.stack); + acc[element.getId()] = [...(acc[element.getId()] || []), error.message]; + + return acc; + }, {} as TValidationErrors); + + setValiationErrors(validationErrors); + + return Boolean(errors.length); + }, [validate, context]); + + const ctx = useMemo( + () => ({ validate: onValidate, errors: validationErrors }), + [validationErrors, onValidate], + ); + + return {children}; +}; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/index.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/index.ts new file mode 100644 index 0000000000..2ae47e498d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/index.ts @@ -0,0 +1,3 @@ +export * from './useValidate'; +export * from './utils/format-value-destination-and-apply-stack-indexes'; +export * from './validate'; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/ui-element.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/ui-element.ts new file mode 100644 index 0000000000..4e42f0544d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/ui-element.ts @@ -0,0 +1,79 @@ +import { testRule } from '@/components/organisms/DynamicUI/rule-engines/utils/execute-rules'; +import { formatValueDestinationAndApplyStackIndexes } from '@/components/providers/Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes'; +import { TValidationParams, UIElementV2 } from '@/components/providers/Validator/types'; +import { IDocumentValueValidatorParams } from '@/components/providers/Validator/value-validators/document.value.validator'; +import { IRequiredValueValidatorParams } from '@/components/providers/Validator/value-validators/required.value-validator'; +import { fieldElelements } from '@/pages/CollectionFlowV2/renderer-schema'; +import { AnyObject } from '@ballerine/ui'; +import get from 'lodash/get'; + +export class UIElement { + constructor(readonly element: UIElementV2, readonly context: unknown, readonly stack: number[]) {} + + getId() { + return this.formatId(this.element.id); + } + + getOriginId() { + return this.element.id; + } + + isRequired() { + const requiredParams = this.element.validation?.required as IRequiredValueValidatorParams; + const documentParams = this.element.validation?.document as IDocumentValueValidatorParams; + + const applyRules = requiredParams?.applyWhen || documentParams?.applyWhen || []; + + if (applyRules.length) { + const isShouldApplyRequired = applyRules.every(rule => + testRule(this.context as AnyObject, rule), + ); + + return Boolean(isShouldApplyRequired); + } else { + return Boolean(requiredParams?.required) || Boolean(documentParams?.documentId); + } + } + + private formatId(id: string) { + return `${id}${this.stack.join('.')}`; + } + + getValueDestination() { + return this.formatValueDestination(this.element.valueDestination); + } + + private formatValueDestination(valueDestination: string) { + return this.formatValueDestinationAndApplyStackIndexes(valueDestination); + } + + private formatValueDestinationAndApplyStackIndexes(valueDestination: string) { + return formatValueDestinationAndApplyStackIndexes(valueDestination, this.stack); + } + + getValue() { + const valueDestination = this.getValueDestination(); + + return get(this.context, valueDestination); + } + + getValidatorsParams() { + const validatorKeys = Object.keys(this.element.validation || {}); + + return validatorKeys.map(key => ({ + validator: key, + params: this.element.validation[key as keyof UIElementV2['validation']] as TValidationParams, + })); + } + + getDefinition() { + return this.element; + } + + getFieldType() { + if (this.element.element === 'fieldlist') return 'field-list'; + if (this.element.element in fieldElelements) return 'field'; + + return 'ui'; + } +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/useValidate.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/useValidate.ts new file mode 100644 index 0000000000..a36c7d1eea --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/useValidate.ts @@ -0,0 +1,24 @@ +import { UIElementV2 } from '@/components/providers/Validator/types'; +import { useCallback } from 'react'; +import { validate as _validate } from './validate'; + +interface IUseValidateParams { + elements: UIElementV2[]; + context: unknown; +} + +export interface IValidationError { + id: string; + message: string; + element: UIElementV2; + valueDestination: string; + stack: number[]; +} + +export const useValidate = ({ elements, context }: IUseValidateParams) => { + const validate = useCallback(() => { + return _validate(elements, context as object); + }, [elements, context]); + + return validate; +}; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts new file mode 100644 index 0000000000..3c5b32e485 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts @@ -0,0 +1,10 @@ +export const formatValueDestinationAndApplyStackIndexes = ( + valueDestination: string, + stack: number[], +) => { + stack.forEach((stackValue, index) => { + valueDestination = valueDestination.replace(`$${index}`, String(stackValue)); + }); + + return valueDestination; +}; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/validate.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/validate.ts new file mode 100644 index 0000000000..bba92367a0 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/validate.ts @@ -0,0 +1,98 @@ +import { testRule } from '@/components/organisms/DynamicUI/rule-engines/utils/execute-rules'; +import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; +import { IValidationError } from '@/components/providers/Validator/hooks/useValidate/useValidate'; +import { IBaseValueValidatorParams, UIElementV2 } from '@/components/providers/Validator/types'; +import { ValueValidatorManager } from '@/components/providers/Validator/value-validator-manager'; +import { AnyObject } from '@ballerine/ui'; + +export const validate = (elements: UIElementV2[], context: object) => { + const validatorManager = new ValueValidatorManager(); + let errors: IValidationError[] = []; + + const fieldValidationStrategy = ( + element: UIElement, + stack: number[] = [], + ): IValidationError[] => { + const value = element.getValue(); + const fieldContext = { + context, + stack, + }; + + const isShouldApplyValidation = ( + params: TParams, + context: AnyObject, + ) => { + const applyRules = params.applyWhen && params.applyWhen ? params.applyWhen : null; + + if (!applyRules) return true; + + return applyRules.length && applyRules.every(rule => testRule(context, rule)); + }; + + const validationErrors = element.getValidatorsParams().map(({ validator, params }) => { + try { + if (validator === 'required' || validator === 'document') { + const isRequired = element.isRequired(); + if (!isRequired && value === undefined) return; + + validatorManager.validate(value, validator as any, params, fieldContext); + } else { + if (!isShouldApplyValidation(params as unknown as IBaseValueValidatorParams, context)) + return; + + if (value === undefined) return; + + validatorManager.validate(value, validator as any, params, fieldContext); + } + } catch (error) { + return { + //@ts-ignore + message: error.message, + element: element.element, + id: element.getId(), + valueDestination: element.getValueDestination(), + stack, + }; + } + }); + + return validationErrors.filter(Boolean) as IValidationError[]; + }; + + const validateFn = (elements: UIElementV2[], context: object, stack: number[] = []) => { + for (let i = 0; i < elements.length; i++) { + const element = elements[i] as UIElementV2; + const uiElement = new UIElement(element, context, stack); + if (!element) continue; + + if (uiElement.getFieldType() === 'field') { + errors = [...errors, ...fieldValidationStrategy(uiElement, stack)]; + + continue; + } + + if (uiElement.getFieldType() === 'field-list') { + const value = uiElement.getValue() as any[]; + + errors = [...errors, ...fieldValidationStrategy(uiElement, stack)]; + + if (!Array.isArray(value)) continue; + + value?.forEach((_, index) => { + validateFn(element.children!, context, [...stack, index]); + }); + + continue; + } + + if (element.children) { + validateFn(element.children, context, stack); + } + } + }; + + validateFn(elements, context as any); + + return errors; +}; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/index.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/index.ts new file mode 100644 index 0000000000..b6fb03d1bc --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/index.ts @@ -0,0 +1 @@ +export * from './useValidatedInput'; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/useValidatedInput.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/useValidatedInput.ts new file mode 100644 index 0000000000..7daa7ec242 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/useValidatedInput.ts @@ -0,0 +1,8 @@ +import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; +import { useValidator } from '@/components/providers/Validator/hooks/useValidator'; + +export const useValidatedInput = (element: UIElement) => { + const { errors } = useValidator(); + + return errors[element.getId()]; +}; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/index.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/index.ts new file mode 100644 index 0000000000..df0ef89dfd --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/index.ts @@ -0,0 +1 @@ +export * from './useValidator'; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/useValidator.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/useValidator.ts new file mode 100644 index 0000000000..f4889599d5 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/useValidator.ts @@ -0,0 +1,4 @@ +import { validatorContext } from '@/components/providers/Validator/validator.context'; +import { useContext } from 'react'; + +export const useValidator = () => useContext(validatorContext); diff --git a/packages/ui/src/components/organisms/Form/_Validator/index.ts b/packages/ui/src/components/organisms/Form/_Validator/index.ts new file mode 100644 index 0000000000..3ed72221f5 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/index.ts @@ -0,0 +1 @@ +export * from './Validator'; diff --git a/packages/ui/src/components/organisms/Form/_Validator/types.ts b/packages/ui/src/components/organisms/Form/_Validator/types.ts new file mode 100644 index 0000000000..77580a1dfb --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/types.ts @@ -0,0 +1,63 @@ +import { IDocumentValueValidatorParams } from '@/components/providers/Validator/value-validators/document.value.validator'; +import { IFormatValueValidatorParams } from '@/components/providers/Validator/value-validators/format.value.validator'; +import { IMaxLengthValueValidatorParams } from '@/components/providers/Validator/value-validators/max-length.value.validator'; +import { IMaximumValueValidatorParams } from '@/components/providers/Validator/value-validators/maximum.value.validator'; +import { IMinLengthValueValidatorParams } from '@/components/providers/Validator/value-validators/min-length.value.validator'; +import { IPatternValidatorParams } from '@/components/providers/Validator/value-validators/pattern.value.validator'; +import { IRequiredValueValidatorParams } from '@/components/providers/Validator/value-validators/required.value-validator'; +import { Rule } from '@/domains/collection-flow'; +import { AnyObject } from '@ballerine/ui'; + +export type TFormats = 'email'; + +export type TValidatorErrorMessage = string; + +export type TValidatorApplyRule = object; + +export type TValidationParams = + | IFormatValueValidatorParams + | IMaxLengthValueValidatorParams + | IMinLengthValueValidatorParams + | IMaximumValueValidatorParams + | IMinLengthValueValidatorParams + | IRequiredValueValidatorParams + | IPatternValidatorParams + | IDocumentValueValidatorParams; + +export type TValidators = + | 'required' + | 'minLength' + | 'maxLength' + | 'pattern' + | 'minimum' + | 'maximum' + | 'format' + | 'document'; + +export interface IBaseFieldParams { + label?: string; + placeholder?: string; + stack?: number[]; +} + +export interface UIElementV2 { + id: string; + element: string; + validation: Partial>; + options?: TFieldParams; + valueDestination: string; + children?: UIElementV2[]; + + availableOn?: Rule[]; + visibleOn?: Rule[]; +} + +export interface IBaseValueValidatorParams { + message?: string; + applyWhen?: Rule[]; +} + +export interface IFieldContext { + context: AnyObject; + stack: number[]; +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/validator.context.ts b/packages/ui/src/components/organisms/Form/_Validator/validator.context.ts new file mode 100644 index 0000000000..427b6885d6 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/validator.context.ts @@ -0,0 +1,12 @@ +import { createContext } from 'react'; + +type TIsValid = boolean; +type TFielName = string; + +export type TValidationErrors = Record; +export interface IValidatorContext { + validate: () => TIsValid; + errors: TValidationErrors; +} + +export const validatorContext = createContext({} as IValidatorContext); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validator-manager.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validator-manager.ts new file mode 100644 index 0000000000..01aea4fa49 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validator-manager.ts @@ -0,0 +1,37 @@ +import { IBaseValueValidatorParams, IFieldContext } from '@/components/providers/Validator/types'; +import { DocumentValueValidator } from '@/components/providers/Validator/value-validators/document.value.validator'; +import { FormatValueValidator } from '@/components/providers/Validator/value-validators/format.value.validator'; +import { MaxLengthValueValidator } from '@/components/providers/Validator/value-validators/max-length.value.validator'; +import { MaximumValueValidator } from '@/components/providers/Validator/value-validators/maximum.value.validator'; +import { MinLengthValueValidator } from '@/components/providers/Validator/value-validators/min-length.value.validator'; +import { MinimumValueValidator } from '@/components/providers/Validator/value-validators/minimum.value.validator'; +import { PatternValueValidator } from '@/components/providers/Validator/value-validators/pattern.value.validator'; +import { RequiredValueValidator } from '@/components/providers/Validator/value-validators/required.value-validator'; + +const validatorsMap = { + required: RequiredValueValidator, + minLength: MinLengthValueValidator, + maxLength: MaxLengthValueValidator, + pattern: PatternValueValidator, + minimum: MinimumValueValidator, + maximum: MaximumValueValidator, + format: FormatValueValidator, + document: DocumentValueValidator, +}; + +export type TValidator = keyof typeof validatorsMap; + +export class ValueValidatorManager { + constructor(readonly validators: typeof validatorsMap = validatorsMap) {} + + validate( + value: unknown, + key: TValidator, + params: TValidatorParams, + fieldContext: IFieldContext, + ) { + const validator = new this.validators[key](params as any); + //@ts-ignore + return validator.validate(value, fieldContext); + } +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.ts new file mode 100644 index 0000000000..6473cd72a0 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.ts @@ -0,0 +1,42 @@ +import { formatValueDestinationAndApplyStackIndexes } from '@/components/providers/Validator/hooks/useValidate'; +import { IBaseValueValidatorParams, IFieldContext } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; +import { Document } from '@/domains/collection-flow'; +import get from 'lodash/get'; + +export interface IDocumentValueValidatorParams extends IBaseValueValidatorParams { + documentId: string; + pathToDocuments: string; + + // Page index to check file id from, defaults to 0 + pageIndex?: number; +} + +export class DocumentValueValidator extends ValueValidator { + type = 'document'; + + validate(_: unknown, fieldContext: IFieldContext): void { + const { pathToDocuments, documentId, pageIndex = 0 } = this.params; + + const documentsPath = this.getDocumentsPathWithIndexes(pathToDocuments, fieldContext); + const documents = get(fieldContext.context, documentsPath); + + const document = documents?.find((document: Document) => document.id === documentId); + + debugger; + + if (!document || !document.pages?.[pageIndex]?.ballerineFileId) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) return `Document is required.`; + + return this.params.message; + } + + private getDocumentsPathWithIndexes(documentsPath: string, fieldContext: IFieldContext) { + return formatValueDestinationAndApplyStackIndexes(documentsPath, fieldContext.stack || []); + } +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.unit.test.ts new file mode 100644 index 0000000000..c932511519 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.unit.test.ts @@ -0,0 +1,186 @@ +import { DocumentValueValidator } from './document.value.validator'; + +describe('DocumentValueValidator', () => { + describe('validation will fail', () => { + test('when document is not found', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'form.documents', + }); + + expect(() => validator.validate(null as any, {} as any)).toThrowError( + 'Document is required.', + ); + }); + + test('when document not found at specific page index', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'form.documents', + pageIndex: 1, + }); + + expect(() => validator.validate(null as any, {} as any)).toThrowError( + 'Document is required.', + ); + }); + + test('when document is not found in nested data structure', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'data.items[$0].documents', + }); + + expect(() => + validator.validate( + null as any, + { + stack: [1], + context: { + data: { + items: [ + { + documents: [], + }, + { + documents: [], + }, + ], + }, + }, + } as any, + ), + ).toThrowError('Document is required.'); + }); + + test('when document not found at specific page index in nested data structure', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'data.items[$0].documents', + pageIndex: 1, + }); + + expect(() => + validator.validate( + null as any, + { + stack: [1], + context: {}, + } as any, + ), + ).toThrowError('Document is required.'); + }); + }); + + describe('validation will pass', () => { + test('when document is found', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'documents', + }); + + const context = { + documents: [ + { + id: '123', + pages: [ + { + ballerineFileId: 'someFileId', + }, + ], + }, + ], + }; + + expect(() => validator.validate(null as any, { context, stack: [] })).not.toThrow(); + }); + + test('when document is found at specific page index', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'documents', + pageIndex: 1, + }); + + const context = { + documents: [ + { + id: '123', + pages: [ + {}, + { + ballerineFileId: 'someFileId', + }, + ], + }, + ], + }; + + expect(() => validator.validate(null as any, { context, stack: [] })).not.toThrow(); + }); + + test.each([ + ['single level nesting', 'data.items[$0].documents', [0]], + ['two levels of nesting', 'data.items[$0].subitems[$1].documents', [0, 1]], + [ + 'three levels of nesting', + 'data.items[$0].subitems[$1].subsubitems[$2].documents', + [0, 1, 1], + ], + ])('when document is found in nested data structure - %s', (_, pathToDocuments, stack) => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments, + }); + + const context = { + data: { + items: [ + { + documents: [{ id: '123', pages: [{ ballerineFileId: 'someFileId' }] }], + subitems: [ + {}, + { + documents: [{ id: '123', pages: [{ ballerineFileId: 'someFileId' }] }], + subsubitems: [ + {}, + { + documents: [{ id: '123', pages: [{ ballerineFileId: 'someFileId' }] }], + }, + ], + }, + ], + }, + ], + }, + }; + + expect(() => validator.validate(null as any, { context, stack })).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'documents', + }); + + expect(() => validator.validate(null as any, { context: {}, stack: [] })).toThrowError( + 'Document is required.', + ); + }); + + test('should return custom error message when message is provided', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'documents', + message: 'Custom error message.', + }); + + expect(() => validator.validate(null as any, { context: {}, stack: [] })).toThrowError( + 'Custom error message.', + ); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.ts new file mode 100644 index 0000000000..16efc39129 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.ts @@ -0,0 +1,29 @@ +import { IBaseValueValidatorParams, TFormats } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; +import EmailValidator from 'email-validator'; + +export interface IFormatValueValidatorParams extends IBaseValueValidatorParams { + format: TFormats; +} + +export class FormatValueValidator extends ValueValidator { + type = 'format'; + + validate(value: unknown): void { + if (this.params.format === 'email') { + if (!EmailValidator.validate(value as string)) { + throw new Error(this.getErrorMessage()); + } + + return; + } + + throw new Error(`Format ${this.params.format} is not supported.`); + } + + private getErrorMessage() { + if (!this.params.message) return 'Invalid format.'; + + return this.params.message.replace('{format}', this.params.format.toString()); + } +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.unit.test.ts new file mode 100644 index 0000000000..47c440d222 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.unit.test.ts @@ -0,0 +1,58 @@ +import { FormatValueValidator } from '@/components/providers/Validator/value-validators/format.value.validator'; + +describe('Format Value Validator', () => { + describe('validation will fail', () => { + test('when value does not match format', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + + test('when unsupported format is provided', () => { + //@ts-ignore + const validator = new FormatValueValidator({ format: 'unsupported', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('Format unsupported is not supported.'); + }); + }); + + describe('validation will pass', () => { + test('when value matches format', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error' }); + + expect(() => validator.validate('example@gmail.com')).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new FormatValueValidator({ format: 'email' }); + + expect(() => validator.validate('abc')).toThrowError('Invalid format.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + + test('should interpolate {format} with the provided value', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error {format}' }); + + expect(() => validator.validate('abc')).toThrowError('error email'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.ts new file mode 100644 index 0000000000..2bc9f8d018 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.ts @@ -0,0 +1,26 @@ +import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; + +export interface IMaxLengthValueValidatorParams extends IBaseValueValidatorParams { + maxLength: number; +} + +export class MaxLengthValueValidator extends ValueValidator { + type = 'maxLength'; + + validate(value: TValue): void { + if (value?.length === undefined || value.length > this.params.maxLength) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) + return `Maximum length is {maxLength}.`.replace( + '{maxLength}', + this.params.maxLength.toString(), + ); + + return this.params.message.replace('{maxLength}', this.params.maxLength.toString()); + } +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.unit.test.ts new file mode 100644 index 0000000000..5f8b758cbb --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.unit.test.ts @@ -0,0 +1,63 @@ +import { MaxLengthValueValidator } from '@/components/providers/Validator/value-validators/max-length.value.validator'; + +describe('Max Length Value Validator', () => { + describe('validation will fail', () => { + test('when value is undefined', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate(undefined as any)).toThrowError('error'); + }); + + test('when value is above maximum length', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate('123456')).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value is below maximum length', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate('12345')).not.toThrow(); + }); + + test('when value is equal to maximum length', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate('12345')).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5 }); + + expect(() => validator.validate('123456')).toThrowError('Maximum length is 5.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate('123456')).toThrowError('error'); + }); + + test('should interpolate {maxLength} with the provided value', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error {maxLength}' }); + + expect(() => validator.validate('123456')).toThrowError('error 5'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate('123456')).toThrowError('error'); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.ts new file mode 100644 index 0000000000..55bf6ae8ce --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.ts @@ -0,0 +1,23 @@ +import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; + +export interface IMaximumValueValidatorParams extends IBaseValueValidatorParams { + maximum: number; +} + +export class MaximumValueValidator extends ValueValidator { + type = 'maximum'; + + validate(value: TValue): void { + if (typeof value !== 'number' || value > this.params.maximum) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) + return `Maximum value is {maximum}.`.replace('{maximum}', this.params.maximum.toString()); + + return this.params.message.replace('{maximum}', this.params.maximum.toString()); + } +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.unit.test.ts new file mode 100644 index 0000000000..4cceba2b16 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.unit.test.ts @@ -0,0 +1,63 @@ +import { MaximumValueValidator } from '@/components/providers/Validator/value-validators/maximum.value.validator'; + +describe('Maximum Value Validator', () => { + describe('validation will fail', () => { + test('when value is undefined', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(undefined as any)).toThrowError('error'); + }); + + test('when value is above maximum', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(6)).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value is below maximum', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(4)).not.toThrow(); + }); + + test('when value is equal to maximum', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(5)).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new MaximumValueValidator({ maximum: 5 }); + + expect(() => validator.validate(6)).toThrowError('Maximum value is 5.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(6)).toThrowError('error'); + }); + + test('should interpolate {maximum} with the provided value', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error {maximum}' }); + + expect(() => validator.validate(6)).toThrowError('error 5'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(6)).toThrowError('error'); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.ts new file mode 100644 index 0000000000..04eb9ff5b3 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.ts @@ -0,0 +1,26 @@ +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; +import { IBaseValueValidatorParams } from '../types'; + +export interface IMinLengthValueValidatorParams extends IBaseValueValidatorParams { + minLength: number; +} + +export class MinLengthValueValidator extends ValueValidator { + type = 'minLength'; + + validate(value: TValue): void { + if (value?.length === undefined || value.length < this.params.minLength) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) + return `Minimum length is {minLength}.`.replace( + '{minLength}', + this.params.minLength.toString(), + ); + + return this.params.message.replace('{minLength}', this.params.minLength.toString()); + } +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.unit.test.ts new file mode 100644 index 0000000000..68e7f607de --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.unit.test.ts @@ -0,0 +1,63 @@ +import { MinLengthValueValidator } from '@/components/providers/Validator/value-validators/min-length.value.validator'; + +describe('MinLength Value Validator', () => { + describe('validation will fail', () => { + test('when value is undefined', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate(undefined as any)).toThrowError('error'); + }); + + test('when value is below minimum length', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate('1234')).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value is above minimum length', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate('12345')).not.toThrow(); + }); + + test('when value is equal to minimum length', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate('12345')).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new MinLengthValueValidator({ minLength: 5 }); + + expect(() => validator.validate('1234')).toThrowError('Minimum length is 5.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate('1234')).toThrowError('error'); + }); + + test('should interpolate {minLength} with the provided value', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error {minLength}' }); + + expect(() => validator.validate('1234')).toThrowError('error 5'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate('1234')).toThrowError('error'); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.ts new file mode 100644 index 0000000000..1e08f15ba4 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.ts @@ -0,0 +1,23 @@ +import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; + +export interface IMinimumValueValidatorParams extends IBaseValueValidatorParams { + minimum: number; +} + +export class MinimumValueValidator extends ValueValidator { + type = 'minimum'; + + validate(value: TValue): void { + if (typeof value !== 'number' || value < this.params.minimum) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) + return `Minimum value is {minimum}.`.replace('{minimum}', this.params.minimum.toString()); + + return this.params.message.replace('{minimum}', this.params.minimum.toString()); + } +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.unit.test.ts new file mode 100644 index 0000000000..61bc62bd29 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.unit.test.ts @@ -0,0 +1,63 @@ +import { MinimumValueValidator } from '@/components/providers/Validator/value-validators/minimum.value.validator'; + +describe('MinimumValueValidator', () => { + describe('validation will fail', () => { + test('when value is undefined', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(undefined as any)).toThrowError('error'); + }); + + test('when value is below minimum', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(4)).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value is above minimum', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(5)).not.toThrow(); + }); + + test('when value is equal to minimum', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(5)).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new MinimumValueValidator({ minimum: 5 }); + + expect(() => validator.validate(4)).toThrowError('Minimum value is 5.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(4)).toThrowError('error'); + }); + + test('should interpolate {minimum} with the provided value', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error {minimum}' }); + + expect(() => validator.validate(4)).toThrowError('error 5'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(4)).toThrowError('error'); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.ts new file mode 100644 index 0000000000..b2cd0302e1 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.ts @@ -0,0 +1,23 @@ +import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; + +export interface IPatternValidatorParams extends IBaseValueValidatorParams { + pattern: string; +} + +export class PatternValueValidator extends ValueValidator { + type = 'pattern'; + + validate(value: unknown) { + if (!new RegExp(this.params.pattern).test(value as string)) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) + return `Value must match {pattern}.`.replace('{pattern}', this.params.pattern); + + return this.params.message.replace('{pattern}', this.params.pattern); + } +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.unit.test.ts new file mode 100644 index 0000000000..b305bcb250 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.unit.test.ts @@ -0,0 +1,54 @@ +import { PatternValueValidator } from '@/components/providers/Validator/value-validators/pattern.value.validator'; + +describe('Pattern Value Validator', () => { + describe('validation will fail', () => { + test('when value does not match pattern', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value matches pattern', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); + + expect(() => validator.validate('123')).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$' }); + + expect(() => validator.validate('abc')).toThrowError('Value must match ^[0-9]+$.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + + test('should interpolate {pattern} with the provided value', () => { + const validator = new PatternValueValidator({ + pattern: '^[0-9]+$', + message: 'error {pattern}', + }); + + expect(() => validator.validate('abc')).toThrowError('error ^[0-9]+$'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.ts new file mode 100644 index 0000000000..bdaaeb20f6 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.ts @@ -0,0 +1,22 @@ +import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; + +export interface IRequiredValueValidatorParams extends IBaseValueValidatorParams { + required: boolean; +} + +export class RequiredValueValidator extends ValueValidator { + type = 'required'; + + validate(value: unknown) { + if (value === undefined || value === null || value === '') { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) return `Value is required.`; + + return this.params.message; + } +} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.unit.test.ts new file mode 100644 index 0000000000..a752045689 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.unit.test.ts @@ -0,0 +1,57 @@ +import { RequiredValueValidator } from '@/components/providers/Validator/value-validators/required.value-validator'; + +describe('Required Value Validator', () => { + describe('validation will fail', () => { + test('when value is undefined', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate(undefined as any)).toThrowError('error'); + }); + + test('when value is empty string', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate('')).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value is not empty string', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate('value')).not.toThrow(); + }); + + test('when value is not null', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate(0)).not.toThrow(); + }); + + test('when value is not undefined', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate(false)).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new RequiredValueValidator({ required: true }); + + expect(() => validator.validate('')).toThrowError('Value is required.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate('')).toThrowError('error'); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/value-validator.abstract.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/value-validator.abstract.ts new file mode 100644 index 0000000000..d45f4d9b13 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/_Validator/value-validators/value-validator.abstract.ts @@ -0,0 +1,9 @@ +import { IFieldContext } from '@/components/providers/Validator/types'; + +export abstract class ValueValidator { + abstract type: string; + + constructor(readonly params: TParams) {} + + abstract validate(value: unknown, fieldContext: IFieldContext): void; +} diff --git a/packages/ui/src/setupTests.ts b/packages/ui/src/setupTests.ts new file mode 100644 index 0000000000..7b6c9edddf --- /dev/null +++ b/packages/ui/src/setupTests.ts @@ -0,0 +1,5 @@ +import '@testing-library/jest-dom'; + +// Mock window and document +global.document = {} as Document; +global.window = {} as Window & typeof globalThis; diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 19e6be8c10..c995bb3e6d 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -1,7 +1,7 @@ import react from '@vitejs/plugin-react'; -import dts from 'vite-plugin-dts'; import fg from 'fast-glob'; import tailwindcss from 'tailwindcss'; +import dts from 'vite-plugin-dts'; import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; @@ -37,6 +37,7 @@ export default defineConfig({ }, plugins: [react(), dts({ copyDtsFiles: true }), tailwindcss(), tsconfigPaths()], test: { + environment: 'jsdom', exclude: ['node_modules', 'dist'], }, build: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c35e554940..5e72a691f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,7 +80,7 @@ importers: version: link:../../packages/common '@ballerine/react-pdf-toolkit': specifier: ^1.2.47 - version: link:../../packages/react-pdf-toolkit + version: 1.2.48(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.15)(@types/react@18.2.37)(date-fns@3.6.0)(react-dom@18.2.0)(react-pdf-tailwind@2.2.1)(react@18.2.0) '@ballerine/ui': specifier: ^0.5.47 version: link:../../packages/ui @@ -357,10 +357,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.25 - version: link:../../packages/config + version: 1.1.26 '@ballerine/eslint-config-react': specifier: ^2.0.25 - version: link:../../packages/eslint-config-react + version: 2.0.26(@ballerine/eslint-config@1.1.26)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -655,10 +655,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.25 - version: link:../../packages/config + version: 1.1.26 '@ballerine/eslint-config-react': specifier: ^2.0.25 - version: link:../../packages/eslint-config-react + version: 2.0.26(@ballerine/eslint-config@1.1.26)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2) '@jest/globals': specifier: ^29.7.0 version: 29.7.0 @@ -902,10 +902,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.25 - version: link:../../packages/config + version: 1.1.26 '@ballerine/eslint-config-react': specifier: ^2.0.25 - version: link:../../packages/eslint-config-react + version: 2.0.26(@ballerine/eslint-config@1.1.26)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -1092,7 +1092,7 @@ importers: dependencies: '@ballerine/react-pdf-toolkit': specifier: ^1.2.45 - version: link:../../packages/react-pdf-toolkit + version: 1.2.48(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react-pdf-tailwind@2.2.1)(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 @@ -1151,10 +1151,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.25 - version: link:../config + version: 1.1.26 '@ballerine/eslint-config': specifier: ^1.1.25 - version: link:../eslint-config + version: 1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@rollup/plugin-babel': specifier: 5.3.1 version: 5.3.1(@babel/core@7.17.9)(@types/babel__core@7.20.4)(rollup@2.70.2) @@ -1232,7 +1232,7 @@ importers: version: 3.7.2(eslint@8.54.0)(typescript@5.1.6) eslint-plugin-import: specifier: ^2.22.0 - version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.54.0) + version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0) eslint-plugin-storybook: specifier: ^0.6.13 version: 0.6.15(eslint@8.54.0)(typescript@5.1.6) @@ -1332,10 +1332,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.25 - version: link:../config + version: 1.1.26 '@ballerine/eslint-config': specifier: ^1.1.25 - version: link:../eslint-config + version: 1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -1410,7 +1410,7 @@ importers: version: 3.7.2(eslint@8.54.0)(typescript@4.9.5) eslint-plugin-import: specifier: ^2.22.0 - version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.54.0) + version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0) eslint-plugin-unused-imports: specifier: ^2.0.0 version: 2.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) @@ -1484,7 +1484,7 @@ importers: dependencies: '@ballerine/eslint-config': specifier: ^1.1.25 - version: link:../eslint-config + version: 1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.56.0) eslint-plugin-react: specifier: ^7.33.2 version: 7.33.2(eslint@8.56.0) @@ -1496,7 +1496,7 @@ importers: dependencies: '@ballerine/config': specifier: ^1.1.25 - version: link:../config + version: 1.1.26 '@ballerine/ui': specifier: 0.5.47 version: link:../ui @@ -1618,10 +1618,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.25 - version: link:../config + version: 1.1.26 '@ballerine/eslint-config': specifier: ^1.1.25 - version: link:../eslint-config + version: 1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -1678,7 +1678,7 @@ importers: version: 3.7.2(eslint@8.54.0)(typescript@4.9.5) eslint-plugin-import: specifier: ^2.22.0 - version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.54.0) + version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0) eslint-plugin-unused-imports: specifier: ^2.0.0 version: 2.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) @@ -1796,6 +1796,9 @@ importers: dayjs: specifier: ^1.11.6 version: 1.11.10 + email-validator: + specifier: ^2.0.4 + version: 2.0.4 i18n-iso-countries: specifier: ^7.6.0 version: 7.7.0 @@ -1835,10 +1838,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.25 - version: link:../config + version: 1.1.26 '@ballerine/eslint-config-react': specifier: ^2.0.25 - version: link:../eslint-config-react + version: 2.0.26(@ballerine/eslint-config@1.1.26)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -1866,6 +1869,12 @@ importers: '@storybook/testing-library': specifier: ^0.0.14-next.2 version: 0.0.14-next.2 + '@testing-library/dom': + specifier: ^10.4.0 + version: 10.4.0 + '@testing-library/react': + specifier: ^13.3.0 + version: 13.4.0(react-dom@18.2.0)(react@18.2.0) '@types/lodash': specifier: ^4.14.191 version: 4.14.201 @@ -1992,10 +2001,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.25 - version: link:../config + version: 1.1.26 '@ballerine/eslint-config': specifier: ^1.1.25 - version: link:../eslint-config + version: 1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -2076,7 +2085,7 @@ importers: version: 3.7.2(eslint@8.54.0)(typescript@5.1.6) eslint-plugin-import: specifier: ^2.22.0 - version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.54.0) + version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0) eslint-plugin-unused-imports: specifier: ^2.0.0 version: 2.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) @@ -2282,10 +2291,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.25 - version: link:../../packages/config + version: 1.1.26 '@ballerine/eslint-config': specifier: ^1.1.25 - version: link:../../packages/eslint-config + version: 1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -2351,7 +2360,7 @@ importers: version: 3.7.2(eslint@8.54.0)(typescript@4.9.5) eslint-plugin-import: specifier: ^2.22.0 - version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.54.0) + version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0) eslint-plugin-unused-imports: specifier: ^2.0.0 version: 2.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) @@ -2424,10 +2433,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.25 - version: link:../../packages/config + version: 1.1.26 '@ballerine/eslint-config': specifier: ^1.1.25 - version: link:../../packages/eslint-config + version: 1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -2487,7 +2496,7 @@ importers: version: 3.7.2(eslint@8.54.0)(typescript@5.1.6) eslint-plugin-import: specifier: ^2.22.0 - version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.54.0) + version: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0) eslint-plugin-unused-imports: specifier: ^2.0.0 version: 2.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) @@ -2831,10 +2840,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.25 - version: link:../../packages/config + version: 1.1.26 '@ballerine/eslint-config': specifier: ^1.1.25 - version: link:../../packages/eslint-config + version: 1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@8.10.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -3009,10 +3018,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.25 - version: link:../../packages/config + version: 1.1.26 '@ballerine/eslint-config': specifier: ^1.1.25 - version: link:../../packages/eslint-config + version: 1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/parser@6.14.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0) eslint: specifier: ^8.46.0 version: 8.54.0 @@ -3021,7 +3030,7 @@ importers: version: 9.0.0(eslint@8.54.0) eslint-config-standard-with-typescript: specifier: ^37.0.0 - version: 37.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.54.0)(typescript@5.5.4) + version: 37.0.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.54.0)(typescript@5.5.4) eslint-plugin-astro: specifier: ^0.28.0 version: 0.28.0(eslint@8.54.0) @@ -4691,13 +4700,6 @@ packages: '@babel/highlight': 7.22.20 chalk: 2.4.2 - /@babel/code-frame@7.23.5: - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.23.4 - chalk: 2.4.2 - /@babel/code-frame@7.24.7: resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} @@ -4769,7 +4771,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@babel/generator': 7.23.6 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) @@ -4836,7 +4838,6 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 - dev: true /@babel/helper-annotate-as-pure@7.22.5: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} @@ -5030,7 +5031,6 @@ packages: '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color - dev: true /@babel/helper-module-transforms@7.23.3(@babel/core@7.17.9): resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} @@ -5201,7 +5201,6 @@ packages: /@babel/helper-string-parser@7.24.8: resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} @@ -5271,14 +5270,6 @@ packages: chalk: 2.4.2 js-tokens: 4.0.0 - /@babel/highlight@7.23.4: - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.22.20 - chalk: 2.4.2 - js-tokens: 4.0.0 - /@babel/highlight@7.24.7: resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} @@ -5316,7 +5307,6 @@ packages: hasBin: true dependencies: '@babel/types': 7.25.6 - dev: true /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.17.9): resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==} @@ -6557,7 +6547,7 @@ packages: '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.24.7 dev: true /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.17.9): @@ -7497,7 +7487,7 @@ packages: resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@babel/parser': 7.23.3 '@babel/types': 7.23.3 @@ -7508,7 +7498,6 @@ packages: '@babel/code-frame': 7.24.7 '@babel/parser': 7.25.6 '@babel/types': 7.25.6 - dev: true /@babel/traverse@7.23.3: resolution: {integrity: sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==} @@ -7558,7 +7547,6 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color - dev: true /@babel/types@7.23.3: resolution: {integrity: sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==} @@ -7592,12 +7580,318 @@ packages: '@babel/helper-string-parser': 7.24.8 '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 - dev: true /@balena/dockerignore@1.0.2: resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} dev: true + /@ballerine/common@0.9.55: + resolution: {integrity: sha512-sp6OPLAJ3VWJ5vXz10+eiU5KQk5A1j5d7R11MvdTnOXEMPp9jGoPMciJOer8zC3YyM+2XZnprd0s3QRCFMu/ZA==} + engines: {node: '>=12'} + dependencies: + '@sinclair/typebox': 0.32.15 + ajv: 8.13.0 + crypto-js: 4.2.0 + dayjs: 1.11.10 + json-schema-to-zod: 0.6.3 + lodash.get: 4.4.2 + lodash.isempty: 4.4.0 + xstate: 5.18.2 + zod: 3.23.4 + dev: false + + /@ballerine/config@1.1.26: + resolution: {integrity: sha512-iHOwUJApE55iwX3m8as6KqKtzqf4kaNq/xM3bKb05YoQzccrubgVYNmwjRIZuWdM/EnuoCFHlbtwRm9rNL8CIw==} + + /@ballerine/eslint-config-react@2.0.26(@ballerine/eslint-config@1.1.26)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2): + resolution: {integrity: sha512-HvvTET3b7v6FoesLhpUVb/ClSDPBRqUsPEmD/xZxSs3IWpCYL3nCnsmhz0yDtBCb4MZs18rTeumZEEF0/wJMQw==} + peerDependencies: + '@ballerine/eslint-config': ^1.1.26 + eslint-plugin-react: ^7.33.2 + eslint-plugin-react-hooks: ^4.6.0 + dependencies: + '@ballerine/eslint-config': 1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0) + eslint-plugin-react: 7.33.2(eslint@8.54.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.54.0) + dev: true + + /@ballerine/eslint-config@1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0): + resolution: {integrity: sha512-Z3+vqCyWTB4wWki8s+uNiJVSECHqDVQSSHP6WHiM0uNL42IKnGAr1VifvzNQowRL4TtaxXGoqNXhJqizKQ8SPQ==} + peerDependencies: + '@stylistic/eslint-plugin-ts': ^1.6.2 + '@typescript-eslint/eslint-plugin': ^6.11.0 + '@typescript-eslint/parser': ^6.11.0 + eslint: ^8.53.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prefer-arrow: ^1.2.3 + eslint-plugin-unused-imports: ^3.0.0 + dependencies: + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@5.1.6) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.1.6) + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.1.6) + eslint: 8.54.0 + eslint-config-prettier: 6.15.0(eslint@8.54.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.54.0) + eslint-plugin-unused-imports: 2.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) + dev: true + + /@ballerine/eslint-config@1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@8.10.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0): + resolution: {integrity: sha512-Z3+vqCyWTB4wWki8s+uNiJVSECHqDVQSSHP6WHiM0uNL42IKnGAr1VifvzNQowRL4TtaxXGoqNXhJqizKQ8SPQ==} + peerDependencies: + '@stylistic/eslint-plugin-ts': ^1.6.2 + '@typescript-eslint/eslint-plugin': ^6.11.0 + '@typescript-eslint/parser': ^6.11.0 + eslint: ^8.53.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prefer-arrow: ^1.2.3 + eslint-plugin-unused-imports: ^3.0.0 + dependencies: + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@4.9.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@4.9.3) + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@4.9.3) + eslint: 8.54.0 + eslint-config-prettier: 8.10.0(eslint@8.54.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.54.0) + eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) + dev: true + + /@ballerine/eslint-config@1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0): + resolution: {integrity: sha512-Z3+vqCyWTB4wWki8s+uNiJVSECHqDVQSSHP6WHiM0uNL42IKnGAr1VifvzNQowRL4TtaxXGoqNXhJqizKQ8SPQ==} + peerDependencies: + '@stylistic/eslint-plugin-ts': ^1.6.2 + '@typescript-eslint/eslint-plugin': ^6.11.0 + '@typescript-eslint/parser': ^6.11.0 + eslint: ^8.53.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prefer-arrow: ^1.2.3 + eslint-plugin-unused-imports: ^3.0.0 + dependencies: + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.5.4) + eslint: 8.54.0 + eslint-config-prettier: 9.0.0(eslint@8.54.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.54.0) + eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) + dev: true + + /@ballerine/eslint-config@1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.56.0): + resolution: {integrity: sha512-Z3+vqCyWTB4wWki8s+uNiJVSECHqDVQSSHP6WHiM0uNL42IKnGAr1VifvzNQowRL4TtaxXGoqNXhJqizKQ8SPQ==} + peerDependencies: + '@stylistic/eslint-plugin-ts': ^1.6.2 + '@typescript-eslint/eslint-plugin': ^6.11.0 + '@typescript-eslint/parser': ^6.11.0 + eslint: ^8.53.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prefer-arrow: ^1.2.3 + eslint-plugin-unused-imports: ^3.0.0 + dependencies: + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.56.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.5.4) + eslint: 8.56.0 + eslint-config-prettier: 9.0.0(eslint@8.56.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.56.0) + eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@6.14.0)(eslint@8.56.0) + dev: false + + /@ballerine/eslint-config@1.1.26(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/parser@6.14.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0): + resolution: {integrity: sha512-Z3+vqCyWTB4wWki8s+uNiJVSECHqDVQSSHP6WHiM0uNL42IKnGAr1VifvzNQowRL4TtaxXGoqNXhJqizKQ8SPQ==} + peerDependencies: + '@stylistic/eslint-plugin-ts': ^1.6.2 + '@typescript-eslint/eslint-plugin': ^6.11.0 + '@typescript-eslint/parser': ^6.11.0 + eslint: ^8.53.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prefer-arrow: ^1.2.3 + eslint-plugin-unused-imports: ^3.0.0 + dependencies: + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.14.0(eslint@8.54.0)(typescript@5.5.4) + eslint: 8.54.0 + eslint-config-prettier: 9.0.0(eslint@8.54.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.54.0) + eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) + dev: true + + /@ballerine/react-pdf-toolkit@1.2.48(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.15)(@types/react@18.2.37)(date-fns@3.6.0)(react-dom@18.2.0)(react-pdf-tailwind@2.2.1)(react@18.2.0): + resolution: {integrity: sha512-I4a+P66bmrfNC1Y4oEdaJaGnxrGzTUu2ILycc8pG5zEK9+KGSY1RWVsI2zJ79G+tq1FuZvykGxvnUH79aIAGXw==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + react-pdf-tailwind: ^2.2.1 + dependencies: + '@ballerine/config': 1.1.26 + '@ballerine/ui': 0.5.48(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.15)(@types/react@18.2.37)(date-fns@3.6.0) + '@react-pdf/renderer': 3.1.14(react@18.2.0) + '@sinclair/typebox': 0.31.26 + ajv: 8.13.0 + ajv-formats: 2.1.1(ajv@8.13.0) + class-variance-authority: 0.7.1 + dayjs: 1.11.10 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-pdf-tailwind: 2.2.1(react@18.2.0)(ts-node@10.9.1) + string-ts: 1.3.3 + transitivePeerDependencies: + - '@babel/runtime' + - '@mui/system' + - '@types/react' + - '@types/react-dom' + - date-fns + - date-fns-jalali + - encoding + - luxon + - moment + - moment-hijri + - moment-jalaali + - supports-color + dev: false + + /@ballerine/react-pdf-toolkit@1.2.48(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react-pdf-tailwind@2.2.1)(react@18.2.0): + resolution: {integrity: sha512-I4a+P66bmrfNC1Y4oEdaJaGnxrGzTUu2ILycc8pG5zEK9+KGSY1RWVsI2zJ79G+tq1FuZvykGxvnUH79aIAGXw==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + react-pdf-tailwind: ^2.2.1 + dependencies: + '@ballerine/config': 1.1.26 + '@ballerine/ui': 0.5.48(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.17)(@types/react@18.2.43) + '@react-pdf/renderer': 3.1.14(react@18.2.0) + '@sinclair/typebox': 0.31.26 + ajv: 8.13.0 + ajv-formats: 2.1.1(ajv@8.13.0) + class-variance-authority: 0.7.1 + dayjs: 1.11.10 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-pdf-tailwind: 2.2.1(react@18.2.0)(ts-node@10.9.1) + string-ts: 1.3.3 + transitivePeerDependencies: + - '@babel/runtime' + - '@mui/system' + - '@types/react' + - '@types/react-dom' + - date-fns + - date-fns-jalali + - encoding + - luxon + - moment + - moment-hijri + - moment-jalaali + - supports-color + dev: false + + /@ballerine/ui@0.5.48(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.15)(@types/react@18.2.37)(date-fns@3.6.0): + resolution: {integrity: sha512-aMqKp5SpnIwYVsbZ0yakDYRz0td+rXcGw3YJP4WtJaVIYlxFhx25j+kZUIW1DQOEv/PcSpEnrnNxr+b210vHVQ==} + dependencies: + '@ballerine/common': 0.9.55 + '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) + '@mui/material': 5.14.18(@emotion/react@11.11.1)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-date-pickers': 6.18.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.18)(@mui/system@5.15.6)(@types/react@18.2.37)(date-fns@3.6.0)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-accordion': 1.1.2(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-checkbox': 1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-hover-card': 1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-icons': 1.3.0(react@18.2.0) + '@radix-ui/react-label': 2.0.2(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popover': 1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-radio-group': 1.1.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-scroll-area': 1.0.5(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.37)(react@18.2.0) + '@rjsf/core': 5.14.2(@rjsf/utils@5.14.2)(react@18.2.0) + '@rjsf/utils': 5.14.2(react@18.2.0) + '@rjsf/validator-ajv8': 5.14.2(@rjsf/utils@5.14.2) + '@tanstack/react-table': 8.10.7(react-dom@18.2.0)(react@18.2.0) + class-variance-authority: 0.6.1 + clsx: 1.2.1 + cmdk: 0.2.0(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + dayjs: 1.11.10 + i18n-iso-countries: 7.7.0 + lodash: 4.17.21 + lucide-react: 0.144.0(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-error-boundary: 4.0.13(react@18.2.0) + react-image: 4.1.0(@babel/runtime@7.23.8)(react-dom@18.2.0)(react@18.2.0) + react-json-view: 1.21.3(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + react-phone-input-2: 2.15.1(react-dom@18.2.0)(react@18.2.0) + string-ts: 1.3.3 + tailwind-merge: 1.14.0 + zod: 3.23.4 + transitivePeerDependencies: + - '@babel/runtime' + - '@mui/system' + - '@types/react' + - '@types/react-dom' + - date-fns + - date-fns-jalali + - encoding + - luxon + - moment + - moment-hijri + - moment-jalaali + - supports-color + dev: false + + /@ballerine/ui@0.5.48(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.17)(@types/react@18.2.43): + resolution: {integrity: sha512-aMqKp5SpnIwYVsbZ0yakDYRz0td+rXcGw3YJP4WtJaVIYlxFhx25j+kZUIW1DQOEv/PcSpEnrnNxr+b210vHVQ==} + dependencies: + '@ballerine/common': 0.9.55 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0) + '@mui/material': 5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-date-pickers': 6.18.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.18)(@mui/system@5.15.6)(@types/react@18.2.43)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-accordion': 1.1.2(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-checkbox': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-hover-card': 1.0.7(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-icons': 1.3.0(react@18.2.0) + '@radix-ui/react-label': 2.0.2(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popover': 1.0.7(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-radio-group': 1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-scroll-area': 1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.43)(react@18.2.0) + '@rjsf/core': 5.14.2(@rjsf/utils@5.14.2)(react@18.2.0) + '@rjsf/utils': 5.14.2(react@18.2.0) + '@rjsf/validator-ajv8': 5.14.2(@rjsf/utils@5.14.2) + '@tanstack/react-table': 8.10.7(react-dom@18.2.0)(react@18.2.0) + class-variance-authority: 0.6.1 + clsx: 1.2.1 + cmdk: 0.2.0(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + dayjs: 1.11.10 + i18n-iso-countries: 7.7.0 + lodash: 4.17.21 + lucide-react: 0.144.0(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-error-boundary: 4.0.13(react@18.2.0) + react-image: 4.1.0(@babel/runtime@7.23.8)(react-dom@18.2.0)(react@18.2.0) + react-json-view: 1.21.3(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + react-phone-input-2: 2.15.1(react-dom@18.2.0)(react@18.2.0) + string-ts: 1.3.3 + tailwind-merge: 1.14.0 + zod: 3.23.4 + transitivePeerDependencies: + - '@babel/runtime' + - '@mui/system' + - '@types/react' + - '@types/react-dom' + - date-fns + - date-fns-jalali + - encoding + - luxon + - moment + - moment-hijri + - moment-jalaali + - supports-color + dev: false + /@base2/pretty-print-object@1.0.1: resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} dev: true @@ -8487,7 +8781,7 @@ packages: /@emotion/babel-plugin@11.11.0: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: - '@babel/helper-module-imports': 7.22.15 + '@babel/helper-module-imports': 7.24.7 '@babel/runtime': 7.23.8 '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 @@ -8498,6 +8792,8 @@ packages: find-root: 1.1.0 source-map: 0.5.7 stylis: 4.2.0 + transitivePeerDependencies: + - supports-color dev: false /@emotion/cache@11.11.0: @@ -8557,6 +8853,31 @@ packages: '@types/react': 18.2.37 hoist-non-react-statics: 3.3.2 react: 18.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@emotion/react@11.11.1(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/babel-plugin': 11.11.0 + '@emotion/cache': 11.11.0 + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + '@types/react': 18.2.43 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + transitivePeerDependencies: + - supports-color dev: false /@emotion/serialize@1.1.2: @@ -8592,6 +8913,31 @@ packages: '@emotion/utils': 1.2.1 '@types/react': 18.2.37 react: 18.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@emotion/styled@11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/babel-plugin': 11.11.0 + '@emotion/is-prop-valid': 1.2.1 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@types/react': 18.2.43 + react: 18.2.0 + transitivePeerDependencies: + - supports-color dev: false /@emotion/unitless@0.8.1: @@ -10393,7 +10739,6 @@ packages: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 - dev: true /@jridgewell/resolve-uri@3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} @@ -10406,7 +10751,6 @@ packages: /@jridgewell/set-array@1.2.1: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - dev: true /@jridgewell/source-map@0.3.5: resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} @@ -10439,7 +10783,6 @@ packages: dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.5.0 - dev: true /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -10758,6 +11101,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@mui/base@5.0.0-beta.24(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bKt2pUADHGQtqWDZ8nvL2Lvg2GNJyd/ZUgZAJoYzRgmnxBL9j36MSlS3+exEdYkikcnvVafcBtD904RypFKb0w==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@floating-ui/react-dom': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.43) + '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react': 18.2.43 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@mui/core-downloads-tracker@5.14.18: resolution: {integrity: sha512-yFpF35fEVDV81nVktu0BE9qn2dD/chs7PsQhlyaV3EnTeZi9RZBuvoEfRym1/jmhJ2tcfeWXiRuHG942mQXJJQ==} dev: false @@ -10798,6 +11164,77 @@ packages: react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) dev: false + /@mui/material@5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-y3UiR/JqrkF5xZR0sIKj6y7xwuEiweh9peiN3Zfjy1gXWXhz5wjlaLdoxFfKIEBUFfeQALxr/Y8avlHH+B9lpQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.14.18 + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.43) + '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-transition-group': 4.4.9 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + dev: false + + /@mui/material@5.14.18(@emotion/react@11.11.1)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-y3UiR/JqrkF5xZR0sIKj6y7xwuEiweh9peiN3Zfjy1gXWXhz5wjlaLdoxFfKIEBUFfeQALxr/Y8avlHH+B9lpQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.14.18 + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.37) + '@mui/utils': 5.15.6(@types/react@18.2.37)(react@18.2.0) + '@types/react': 18.2.37 + '@types/react-transition-group': 4.4.9 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + dev: false + /@mui/private-theming@5.15.6(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-ZBX9E6VNUSscUOtU8uU462VvpvBS7eFl5VfxAzTRVQBHflzL+5KtnGrebgf6Nd6cdvxa1o0OomiaxSKoN2XDmg==} engines: {node: '>=12.0.0'} @@ -10815,6 +11252,23 @@ packages: react: 18.2.0 dev: false + /@mui/private-theming@5.15.6(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-ZBX9E6VNUSscUOtU8uU462VvpvBS7eFl5VfxAzTRVQBHflzL+5KtnGrebgf6Nd6cdvxa1o0OomiaxSKoN2XDmg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@mui/styled-engine@5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): resolution: {integrity: sha512-KAn8P8xP/WigFKMlEYUpU9z2o7jJnv0BG28Qu1dhNQVutsLVIFdRf5Nb+0ijp2qgtcmygQ0FtfRuXv5LYetZTg==} engines: {node: '>=12.0.0'} @@ -10867,6 +11321,36 @@ packages: react: 18.2.0 dev: false + /@mui/system@5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-J01D//u8IfXvaEHMBQX5aO2l7Q+P15nt96c4NskX7yp5/+UuZP8XCQJhtBtLuj+M2LLyXHYGmCPeblsmmscP2Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0) + '@mui/private-theming': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@mui/styled-engine': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.43) + '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@mui/types@7.2.13(@types/react@18.2.37): resolution: {integrity: sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==} peerDependencies: @@ -10878,6 +11362,17 @@ packages: '@types/react': 18.2.37 dev: false + /@mui/types@7.2.13(@types/react@18.2.43): + resolution: {integrity: sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.43 + dev: false + /@mui/utils@5.15.6(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-qfEhf+zfU9aQdbzo1qrSWlbPQhH1nCgeYgwhOVnj9Bn39shJQitEnXpSQpSNag8+uty5Od6PxmlNKPTnPySRKA==} engines: {node: '>=12.0.0'} @@ -10896,6 +11391,80 @@ packages: react-is: 18.2.0 dev: false + /@mui/utils@5.15.6(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-qfEhf+zfU9aQdbzo1qrSWlbPQhH1nCgeYgwhOVnj9Bn39shJQitEnXpSQpSNag8+uty5Od6PxmlNKPTnPySRKA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@types/prop-types': 15.7.11 + '@types/react': 18.2.43 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + + /@mui/x-date-pickers@6.18.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.18)(@mui/system@5.15.6)(@types/react@18.2.37)(date-fns@3.6.0)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-22gWCzejBGG4Kycpk/yYhzV6Pj/xVBvaz5A1rQmCD/0DCXTDJvXPG8qvzrNlp1wj8q+rAQO82e/+conUGgYgYg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.14.18(@emotion/react@11.11.1)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) + '@mui/utils': 5.15.6(@types/react@18.2.37)(react@18.2.0) + '@types/react-transition-group': 4.4.9 + clsx: 2.1.0 + date-fns: 3.6.0 + dayjs: 1.11.10 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /@mui/x-date-pickers@6.18.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.18)(@mui/system@5.15.6)(@types/react@18.2.37)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-22gWCzejBGG4Kycpk/yYhzV6Pj/xVBvaz5A1rQmCD/0DCXTDJvXPG8qvzrNlp1wj8q+rAQO82e/+conUGgYgYg==} engines: {node: '>=14.0.0'} @@ -10951,6 +11520,61 @@ packages: - '@types/react' dev: false + /@mui/x-date-pickers@6.18.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.18)(@mui/system@5.15.6)(@types/react@18.2.43)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-22gWCzejBGG4Kycpk/yYhzV6Pj/xVBvaz5A1rQmCD/0DCXTDJvXPG8qvzrNlp1wj8q+rAQO82e/+conUGgYgYg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react@18.2.0) + '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@types/react-transition-group': 4.4.9 + clsx: 2.1.0 + dayjs: 1.11.10 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /@ndelangen/get-tarball@3.0.9: resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==} dependencies: @@ -11631,6 +12255,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-accordion@1.1.2(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-arrow@1.0.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==} peerDependencies: @@ -11682,7 +12335,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-aspect-ratio@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-fXR5kbMan9oQqMuacfzlGG/SQMcmMlZ4wrvpckv8SgUulD0MMpspxJrxg/Gp/ISV3JfV1AeSWTYK9GvxA4ySwA==} @@ -11757,6 +12409,34 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} peerDependencies: @@ -11785,6 +12465,34 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} peerDependencies: @@ -11830,7 +12538,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-collection@1.1.0(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} @@ -11889,7 +12596,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} @@ -11904,6 +12610,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.43 + react: 18.2.0 + dev: false + /@radix-ui/react-context@1.0.0(react@18.2.0): resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==} peerDependencies: @@ -11938,7 +12657,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-context@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} @@ -11980,6 +12698,33 @@ packages: - '@types/react' dev: false + /@radix-ui/react-dialog@1.0.0(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.0 + '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) + '@radix-ui/react-context': 1.0.0(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.0(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.0(react@18.2.0) + '@radix-ui/react-portal': 1.0.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.0(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0) + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.4(@types/react@18.2.43)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /@radix-ui/react-dialog@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==} peerDependencies: @@ -12014,6 +12759,40 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.37)(react@18.2.0) dev: false + /@radix-ui/react-dialog@1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.43)(react@18.2.0) + dev: false + /@radix-ui/react-direction@1.0.1(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: @@ -12039,7 +12818,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-direction@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} @@ -12133,7 +12911,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} @@ -12160,6 +12937,31 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} peerDependencies: @@ -12187,6 +12989,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.0(react@18.2.0): resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==} peerDependencies: @@ -12221,7 +13050,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-focus-scope@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==} @@ -12280,7 +13108,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} @@ -12305,6 +13132,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-hover-card@1.0.2(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-LOqJAHdjjLoIhOCHdFO5ASkNACG/wwPQljzrm4U53n1Uxa1Crheazs82dST1946zgu4p0U4IrFmuQ6PTODIlkw==} peerDependencies: @@ -12356,6 +13206,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-hover-card@1.0.7(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-icons@1.3.0(react@18.2.0): resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: @@ -12401,7 +13280,6 @@ packages: '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-id@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} @@ -12438,6 +13316,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} peerDependencies: @@ -12476,6 +13375,44 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.37)(react@18.2.0) dev: false + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.43)(react@18.2.0) + dev: false + /@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==} peerDependencies: @@ -12511,6 +13448,41 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.37)(react@18.2.0) dev: false + /@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.43)(react@18.2.0) + dev: false + /@radix-ui/react-popper@1.0.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-J4Vj7k3k+EHNWgcKrE+BLlQfpewxA7Zd76h5I0bIa+/EqaIZ3DuwrbPj49O3wqN+STnXsBuxiHLiF0iU3yfovw==} peerDependencies: @@ -12622,6 +13594,36 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@floating-ui/react-dom': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/rect': 1.0.1 + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-portal@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==} peerDependencies: @@ -12685,7 +13687,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} @@ -12708,6 +13709,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==} peerDependencies: @@ -12743,6 +13765,28 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-primitive@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==} peerDependencies: @@ -12806,7 +13850,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} @@ -12858,6 +13901,36 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-radio-group@1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -12913,7 +13986,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} @@ -12972,6 +14044,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-scroll-area@1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-select@1.2.2(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} peerDependencies: @@ -13181,7 +14282,6 @@ packages: '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-slot@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} @@ -13197,6 +14297,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-slot@1.1.0(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + react: 18.2.0 + dev: false + /@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==} peerDependencies: @@ -13572,7 +14686,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} @@ -13624,7 +14737,6 @@ packages: '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} @@ -13687,7 +14799,6 @@ packages: '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-layout-effect@1.0.0(react@18.2.0): resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==} @@ -13723,7 +14834,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} @@ -13763,7 +14873,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-rect@1.0.0(react@18.2.0): resolution: {integrity: sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==} @@ -13802,7 +14911,6 @@ packages: '@radix-ui/rect': 1.0.1 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-size@1.0.0(react@18.2.0): resolution: {integrity: sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==} @@ -13841,7 +14949,6 @@ packages: '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} @@ -17303,6 +18410,34 @@ packages: espree: 9.6.1 dev: false + /@stylistic/eslint-plugin-js@1.6.2(eslint@8.54.0): + resolution: {integrity: sha512-ndT6X2KgWGxv8101pdMOxL8pihlYIHcOv3ICd70cgaJ9exwkPn8hJj4YQwslxoAlre1TFHnXd/G1/hYXgDrjIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@types/eslint': 8.56.5 + acorn: 8.11.3 + escape-string-regexp: 4.0.0 + eslint: 8.54.0 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + dev: true + + /@stylistic/eslint-plugin-js@1.6.2(eslint@8.56.0): + resolution: {integrity: sha512-ndT6X2KgWGxv8101pdMOxL8pihlYIHcOv3ICd70cgaJ9exwkPn8hJj4YQwslxoAlre1TFHnXd/G1/hYXgDrjIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@types/eslint': 8.56.5 + acorn: 8.11.3 + escape-string-regexp: 4.0.0 + eslint: 8.56.0 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + dev: false + /@stylistic/eslint-plugin-ts@1.6.2(eslint@8.53.0)(typescript@5.5.4): resolution: {integrity: sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -17318,6 +18453,66 @@ packages: - typescript dev: false + /@stylistic/eslint-plugin-ts@1.6.2(eslint@8.54.0)(typescript@4.9.3): + resolution: {integrity: sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@stylistic/eslint-plugin-js': 1.6.2(eslint@8.54.0) + '@types/eslint': 8.56.5 + '@typescript-eslint/utils': 6.21.0(eslint@8.54.0)(typescript@4.9.3) + eslint: 8.54.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@stylistic/eslint-plugin-ts@1.6.2(eslint@8.54.0)(typescript@5.1.6): + resolution: {integrity: sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@stylistic/eslint-plugin-js': 1.6.2(eslint@8.54.0) + '@types/eslint': 8.56.5 + '@typescript-eslint/utils': 6.21.0(eslint@8.54.0)(typescript@5.1.6) + eslint: 8.54.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@stylistic/eslint-plugin-ts@1.6.2(eslint@8.54.0)(typescript@5.5.4): + resolution: {integrity: sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@stylistic/eslint-plugin-js': 1.6.2(eslint@8.54.0) + '@types/eslint': 8.56.5 + '@typescript-eslint/utils': 6.21.0(eslint@8.54.0)(typescript@5.5.4) + eslint: 8.54.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@stylistic/eslint-plugin-ts@1.6.2(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@stylistic/eslint-plugin-js': 1.6.2(eslint@8.56.0) + '@types/eslint': 8.56.5 + '@typescript-eslint/utils': 6.21.0(eslint@8.56.0)(typescript@5.5.4) + eslint: 8.56.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.5.2)(svelte@3.59.2)(vite@4.5.3): resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==} engines: {node: ^14.18.0 || >= 16} @@ -17764,11 +18959,25 @@ packages: seedrandom: 2.4.3 dev: false + /@testing-library/dom@10.4.0: + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/runtime': 7.23.8 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + /@testing-library/dom@8.20.1: resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} engines: {node: '>=12'} dependencies: - '@babel/code-frame': 7.22.13 + '@babel/code-frame': 7.24.7 '@babel/runtime': 7.23.8 '@types/aria-query': 5.0.4 aria-query: 5.1.3 @@ -17782,7 +18991,7 @@ packages: resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} engines: {node: '>=14'} dependencies: - '@babel/code-frame': 7.22.13 + '@babel/code-frame': 7.24.7 '@babel/runtime': 7.23.8 '@types/aria-query': 5.0.4 aria-query: 5.1.3 @@ -17846,9 +19055,9 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 '@testing-library/dom': 8.20.1 - '@types/react-dom': 18.2.15 + '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true @@ -18628,7 +19837,6 @@ packages: dependencies: '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 - dev: false /@types/estree-jsx@1.0.3: resolution: {integrity: sha512-pvQ+TKeRHeiUGRhvYwRrQ/ISnohKkSJR14fT2yqyZ4e9K5vqc7hrtY2Y1Dw0ZwAzQ6DQsxsaCUuSIIi8v0Cq6w==} @@ -19089,7 +20297,6 @@ packages: resolution: {integrity: sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==} dependencies: '@types/react': 18.2.43 - dev: true /@types/react-helmet@6.1.9: resolution: {integrity: sha512-nuOeTefP4yPTWHvjGksCBKb/4hsgJxSX7aSTjTIDFXJIkZ6Wo2Y4/cmE1FO9OlYBrHjKOer/0zLwY7s4qiQBtw==} @@ -19116,7 +20323,6 @@ packages: '@types/prop-types': 15.7.10 '@types/scheduler': 0.16.6 csstype: 3.1.2 - dev: true /@types/react@18.2.46: resolution: {integrity: sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w==} @@ -19557,6 +20763,35 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/type-utils': 6.14.0(eslint@8.56.0)(typescript@5.5.4) + '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.3.0 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/experimental-utils@4.33.0(eslint@8.54.0)(typescript@4.9.5): resolution: {integrity: sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==} engines: {node: ^10.12.0 || >=12.0.0} @@ -19734,6 +20969,27 @@ packages: - supports-color dev: false + /@typescript-eslint/parser@6.14.0(eslint@8.54.0)(typescript@5.5.4): + resolution: {integrity: sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.54.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@6.14.0(eslint@8.55.0)(typescript@5.2.2): resolution: {integrity: sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -19755,6 +21011,27 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@6.14.0(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.56.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/scope-manager@4.33.0: resolution: {integrity: sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==} engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} @@ -19785,7 +21062,6 @@ packages: dependencies: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/visitor-keys': 6.14.0 - dev: true /@typescript-eslint/scope-manager@6.21.0: resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} @@ -19793,7 +21069,6 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - dev: false /@typescript-eslint/type-utils@5.62.0(eslint@8.22.0)(typescript@4.9.5): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} @@ -19955,6 +21230,26 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils@6.14.0(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.5.4) + '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.5.4) + debug: 4.3.6 + eslint: 8.56.0 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/types@4.33.0: resolution: {integrity: sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==} engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} @@ -19973,12 +21268,10 @@ packages: /@typescript-eslint/types@6.14.0: resolution: {integrity: sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==} engines: {node: ^16.0.0 || >=18.0.0} - dev: true /@typescript-eslint/types@6.21.0: resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} - dev: false /@typescript-eslint/typescript-estree@4.33.0(typescript@4.9.5): resolution: {integrity: sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==} @@ -20148,6 +21441,70 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@6.14.0(typescript@5.5.4): + resolution: {integrity: sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.6 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + /@typescript-eslint/typescript-estree@6.21.0(typescript@4.9.3): + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.6 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@4.9.3) + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.1.6): + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.6 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.5.4): resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -20168,7 +21525,6 @@ packages: typescript: 5.5.4 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/utils@5.62.0(eslint@8.22.0)(typescript@4.9.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} @@ -20328,6 +21684,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@6.14.0(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.5.4) + eslint: 8.56.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /@typescript-eslint/utils@6.21.0(eslint@8.53.0)(typescript@5.5.4): resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -20347,6 +21722,82 @@ packages: - typescript dev: false + /@typescript-eslint/utils@6.21.0(eslint@8.54.0)(typescript@4.9.3): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@4.9.3) + eslint: 8.54.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@6.21.0(eslint@8.54.0)(typescript@5.1.6): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) + eslint: 8.54.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@6.21.0(eslint@8.54.0)(typescript@5.5.4): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) + eslint: 8.54.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@6.21.0(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) + eslint: 8.56.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /@typescript-eslint/visitor-keys@4.33.0: resolution: {integrity: sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==} engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} @@ -20377,7 +21828,6 @@ packages: dependencies: '@typescript-eslint/types': 6.14.0 eslint-visitor-keys: 3.4.3 - dev: true /@typescript-eslint/visitor-keys@6.21.0: resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} @@ -20385,7 +21835,6 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 eslint-visitor-keys: 3.4.3 - dev: false /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -21306,6 +22755,17 @@ packages: dependencies: ajv: 8.12.0 + /ajv-formats@2.1.1(ajv@8.13.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.13.0 + dev: false + /ajv-formats@3.0.1(ajv@8.13.0): resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -21357,7 +22817,6 @@ packages: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 uri-js: 4.4.1 - dev: true /ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -22895,6 +24354,20 @@ packages: - '@types/react' dev: false + /cmdk@0.2.0(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@radix-ui/react-dialog': 1.0.0(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + command-score: 0.1.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -24645,6 +26118,11 @@ packages: batch-processor: 1.0.0 dev: true + /email-validator@2.0.4: + resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==} + engines: {node: '>4.0'} + dev: false + /embla-carousel-react@8.0.0-rc11(react@18.2.0): resolution: {integrity: sha512-hXOAUMOIa0GF5BtdTTqBuKcjgU+ipul6thTCXOZttqnu2c6VS3SIzUUT+onIIEw+AptzKJcPwGcoAByAGa9eJw==} peerDependencies: @@ -25377,7 +26855,16 @@ packages: eslint: 8.54.0 dev: true - /eslint-config-standard-with-typescript@37.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.54.0)(typescript@5.5.4): + /eslint-config-prettier@9.0.0(eslint@8.56.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.56.0 + dev: false + + /eslint-config-standard-with-typescript@37.0.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.54.0)(typescript@5.5.4): resolution: {integrity: sha512-V8I/Q1eFf9tiOuFHkbksUdWO3p1crFmewecfBtRxXdnvb71BCJx+1xAknlIRZMwZioMX3/bPtMVCZsf1+AjjOw==} peerDependencies: '@typescript-eslint/eslint-plugin': ^5.52.0 @@ -25387,11 +26874,10 @@ packages: eslint-plugin-promise: ^6.0.0 typescript: '*' dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.5.4) '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.5.4) eslint: 8.54.0 eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.54.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint@8.54.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.54.0) eslint-plugin-n: 16.6.2(eslint@8.54.0) eslint-plugin-promise: 6.1.1(eslint@8.54.0) typescript: 5.5.4 @@ -25409,7 +26895,7 @@ packages: eslint-plugin-promise: ^6.0.0 dependencies: eslint: 8.54.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint@8.54.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.54.0) eslint-plugin-n: 16.6.2(eslint@8.54.0) eslint-plugin-promise: 6.1.1(eslint@8.54.0) dev: true @@ -25506,6 +26992,64 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.1.6) + debug: 3.2.7 + eslint: 8.54.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.14.0(eslint@8.54.0)(typescript@5.5.4) + debug: 3.2.7 + eslint: 8.54.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-astro@0.28.0(eslint@8.54.0): resolution: {integrity: sha512-fZ3B93nXLSXMmEYSAnHkDRBKDbUFuIkWj5CoKE4fxjPnE/EZEHu6zxtX2UJZeclJKu33Uf2mWdeCJKFufyracg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -25664,7 +27208,42 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0)(eslint@8.54.0): + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0): + resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.1.6) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.54.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.54.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -25674,7 +27253,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.14.0(eslint@8.54.0)(typescript@5.5.4) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 @@ -25683,7 +27262,7 @@ packages: doctrine: 2.1.0 eslint: 8.54.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.54.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -25727,6 +27306,22 @@ packages: eslint: 8.53.0 dev: false + /eslint-plugin-prefer-arrow@1.2.3(eslint@8.54.0): + resolution: {integrity: sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ==} + peerDependencies: + eslint: '>=2.0.0' + dependencies: + eslint: 8.54.0 + dev: true + + /eslint-plugin-prefer-arrow@1.2.3(eslint@8.56.0): + resolution: {integrity: sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ==} + peerDependencies: + eslint: '>=2.0.0' + dependencies: + eslint: 8.56.0 + dev: false + /eslint-plugin-promise@6.1.1(eslint@8.54.0): resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -25821,6 +27416,31 @@ packages: string.prototype.matchall: 4.0.10 dev: true + /eslint-plugin-react@7.33.2(eslint@8.54.0): + resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.7 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.2 + doctrine: 2.1.0 + es-iterator-helpers: 1.0.15 + eslint: 8.54.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.7 + object.fromentries: 2.0.7 + object.hasown: 1.1.3 + object.values: 1.1.7 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.10 + dev: true + /eslint-plugin-react@7.33.2(eslint@8.56.0): resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} engines: {node: '>=4'} @@ -25941,7 +27561,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.22.0)(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.22.0)(typescript@5.5.4) eslint: 8.22.0 eslint-rule-composer: 0.3.0 dev: true @@ -25956,7 +27576,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.1.6) eslint: 8.54.0 eslint-rule-composer: 0.3.0 dev: true @@ -25991,6 +27611,21 @@ packages: eslint-rule-composer: 0.3.0 dev: false + /eslint-plugin-unused-imports@3.0.0(@typescript-eslint/eslint-plugin@6.14.0)(eslint@8.56.0): + resolution: {integrity: sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^6.0.0 + eslint: ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.5.4) + eslint: 8.56.0 + eslint-rule-composer: 0.3.0 + dev: false + /eslint-rule-composer@0.3.0: resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} engines: {node: '>=4.0.0'} @@ -26994,7 +28629,7 @@ packages: typescript: '>3.6.0' webpack: ^5.11.0 dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 chalk: 4.1.2 chokidar: 3.5.3 cosmiconfig: 7.1.0 @@ -29262,7 +30897,7 @@ packages: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -32699,7 +34334,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -33944,6 +35579,23 @@ packages: - encoding dev: false + /react-json-view@1.21.3(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==} + peerDependencies: + react: ^17.0.0 || ^16.3.0 || ^15.5.4 + react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4 + dependencies: + flux: 4.0.4(react@18.2.0) + react: 18.2.0 + react-base16-styling: 0.6.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + react-textarea-autosize: 8.5.3(@types/react@18.2.43)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - encoding + dev: false + /react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==} peerDependencies: @@ -34002,7 +35654,6 @@ packages: - encoding - react - ts-node - dev: true /react-phone-input-2@2.15.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-W03abwhXcwUoq+vUFvC6ch2+LJYMN8qSOiO889UH6S7SyMCQvox/LF3QWt+cZagZrRdi5z2ON3omnjoCUmlaYw==} @@ -34054,7 +35705,6 @@ packages: react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.2.43)(react@18.2.0) tslib: 2.6.2 - dev: true /react-remove-scroll@2.5.4(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==} @@ -34075,6 +35725,25 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.37)(react@18.2.0) dev: false + /react-remove-scroll@2.5.4(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.43 + react: 18.2.0 + react-remove-scroll-bar: 2.3.4(@types/react@18.2.43)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.43)(react@18.2.0) + tslib: 2.6.2 + use-callback-ref: 1.3.0(@types/react@18.2.43)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.43)(react@18.2.0) + dev: false + /react-remove-scroll@2.5.5(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} engines: {node: '>=10'} @@ -34110,7 +35779,6 @@ packages: tslib: 2.6.2 use-callback-ref: 1.3.0(@types/react@18.2.43)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.43)(react@18.2.0) - dev: true /react-resize-detector@7.1.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==} @@ -34219,7 +35887,6 @@ packages: invariant: 2.2.4 react: 18.2.0 tslib: 2.6.2 - dev: true /react-textarea-autosize@8.5.3(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==} @@ -34235,6 +35902,20 @@ packages: - '@types/react' dev: false + /react-textarea-autosize@8.5.3(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.23.8 + react: 18.2.0 + use-composed-ref: 1.3.0(react@18.2.0) + use-latest: 1.2.1(@types/react@18.2.43)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} peerDependencies: @@ -36873,6 +38554,24 @@ packages: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} dev: false + /ts-api-utils@1.0.3(typescript@4.9.3): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 4.9.3 + dev: true + + /ts-api-utils@1.0.3(typescript@5.1.6): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.1.6 + dev: true + /ts-api-utils@1.0.3(typescript@5.2.2): resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} engines: {node: '>=16.13.0'} @@ -36889,7 +38588,6 @@ packages: typescript: '>=4.2.0' dependencies: typescript: 5.5.4 - dev: false /ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} @@ -37824,7 +39522,7 @@ packages: dependencies: browserslist: 4.22.1 escalade: 3.1.1 - picocolors: 1.0.0 + picocolors: 1.0.1 /update-browserslist-db@1.0.13(browserslist@4.22.2): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} @@ -37834,7 +39532,7 @@ packages: dependencies: browserslist: 4.22.2 escalade: 3.1.1 - picocolors: 1.0.0 + picocolors: 1.0.1 dev: true /update-browserslist-db@1.1.0(browserslist@4.23.3): @@ -37899,7 +39597,6 @@ packages: '@types/react': 18.2.43 react: 18.2.0 tslib: 2.6.2 - dev: true /use-composed-ref@1.3.0(react@18.2.0): resolution: {integrity: sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==} @@ -37931,6 +39628,19 @@ packages: react: 18.2.0 dev: false + /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.43 + react: 18.2.0 + dev: false + /use-latest@1.2.1(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==} peerDependencies: @@ -37945,6 +39655,20 @@ packages: use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.37)(react@18.2.0) dev: false + /use-latest@1.2.1(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.43 + react: 18.2.0 + use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.43)(react@18.2.0) + dev: false + /use-query-params@2.2.1(react-dom@18.2.0)(react-router-dom@6.19.0)(react@18.2.0): resolution: {integrity: sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==} peerDependencies: @@ -38004,7 +39728,6 @@ packages: detect-node-es: 1.1.0 react: 18.2.0 tslib: 2.6.2 - dev: true /use-sync-external-store@1.2.0(react@18.2.0): resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} From b2c7a448a5fe56272b717bb1c61677ec60123d2a Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Tue, 3 Dec 2024 17:11:04 +0200 Subject: [PATCH 02/54] feat: implmeneted validate method & tests --- .../organisms/Form/Validator/types/index.ts | 3 + .../create-validation-error.ts | 27 + .../create-validation-error.unit.test.ts | 58 ++ .../utils/create-validation-error/index.ts | 1 + .../Validator/utils/format-id/format-id.ts | 7 + .../utils/format-id/format-id.unit.test.ts | 31 + .../Form/Validator/utils/format-id/index.ts | 1 + .../format-value-destination.ts | 11 + .../format-value-destination.unit.test.ts | 45 ++ .../utils/format-value-destination/index.ts | 1 + .../Form/Validator/utils/validate/types.ts | 3 + .../Form/Validator/utils/validate/validate.ts | 69 ++ .../utils/validate/validate.unit.test.ts | 730 ++++++++++++++++++ .../validators/format/format-validator.ts | 2 + .../format/format.validator.unit.test.ts | 6 +- .../max-length/max-length-validator.ts | 4 + .../max-length.validator.unit.test.ts | 14 +- .../validators/maximum/maximum-validator.ts | 2 + .../maximum/maximum.validator.unit.test.ts | 6 + .../min-length/min-length-validator.ts | 4 +- .../min-length.validator.unit.test.ts | 6 +- .../minimum/minimum-value-validator.ts | 2 + .../minimum/minimum.validator.unit.test.ts | 13 + .../validators/pattern/pattern-validator.ts | 2 + .../pattern/pattern.validator.unit.test.ts | 8 +- 25 files changed, 1035 insertions(+), 21 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/format-id/format-id.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/format-id/format-id.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/format-id/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/validate/types.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts diff --git a/packages/ui/src/components/organisms/Form/Validator/types/index.ts b/packages/ui/src/components/organisms/Form/Validator/types/index.ts index 3525118cbd..2ff685523b 100644 --- a/packages/ui/src/components/organisms/Form/Validator/types/index.ts +++ b/packages/ui/src/components/organisms/Form/Validator/types/index.ts @@ -22,6 +22,7 @@ export interface ICommonValidator export interface IValidationSchema { id: string; + valueDestination?: string; validators: ICommonValidator[]; children?: IValidationSchema[]; } @@ -39,3 +40,5 @@ export type TValidator = ( value: T, validator: ICommonValidator, ) => void; + +export type TDeepthLevelStack = number[]; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.ts b/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.ts new file mode 100644 index 0000000000..009e1c1edc --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.ts @@ -0,0 +1,27 @@ +import { TDeepthLevelStack } from '../../types'; + +import { IValidationError } from '../../types'; +import { formatId } from '../format-id'; + +export const createValidationError = ({ + id, + invalidValue, + message, + stack, +}: { + id: string; + invalidValue: unknown; + message: string; + stack: TDeepthLevelStack; +}): IValidationError => { + const formattedId = formatId(id, stack); + + const error: IValidationError = { + id: formattedId, + originId: id, + invalidValue, + message: [message], + }; + + return error; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.unit.test.ts new file mode 100644 index 0000000000..4a12fe744d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.unit.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest'; +import { createValidationError } from './create-validation-error'; + +describe('createValidationError', () => { + it('should create validation error with formatted id', () => { + const params = { + id: 'test', + invalidValue: 'invalid', + message: 'error message', + stack: [1, 2], + }; + + const result = createValidationError(params); + + expect(result).toEqual({ + id: 'test-1-2', + originId: 'test', + invalidValue: 'invalid', + message: ['error message'], + }); + }); + + it('should handle empty stack', () => { + const params = { + id: 'test', + invalidValue: 123, + message: 'error message', + stack: [], + }; + + const result = createValidationError(params); + + expect(result).toEqual({ + id: 'test-', + originId: 'test', + invalidValue: 123, + message: ['error message'], + }); + }); + + it('should handle single stack value', () => { + const params = { + id: 'test', + invalidValue: null, + message: 'error message', + stack: [1], + }; + + const result = createValidationError(params); + + expect(result).toEqual({ + id: 'test-1', + originId: 'test', + invalidValue: null, + message: ['error message'], + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/index.ts b/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/index.ts new file mode 100644 index 0000000000..a38bffa57d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/index.ts @@ -0,0 +1 @@ +export * from './create-validation-error'; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/format-id/format-id.ts b/packages/ui/src/components/organisms/Form/Validator/utils/format-id/format-id.ts new file mode 100644 index 0000000000..2b7affbc34 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/format-id/format-id.ts @@ -0,0 +1,7 @@ +import { TDeepthLevelStack } from '../../types'; + +export const formatId = (id: string, stack: TDeepthLevelStack) => { + const _id = `${id}${stack.length ? `-${stack.join('-')}` : ''}`; + + return _id; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/format-id/format-id.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/format-id/format-id.unit.test.ts new file mode 100644 index 0000000000..fc124f375a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/format-id/format-id.unit.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { formatId } from './format-id'; + +describe('formatId', () => { + it('should append stack values to id', () => { + const id = 'test'; + const stack = [1, 2]; + + const result = formatId(id, stack); + + expect(result).toBe('test-1-2'); + }); + + it('should handle empty stack', () => { + const id = 'test'; + const stack: number[] = []; + + const result = formatId(id, stack); + + expect(result).toBe('test'); + }); + + it('should handle single stack value', () => { + const id = 'test'; + const stack = [1]; + + const result = formatId(id, stack); + + expect(result).toBe('test-1'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/format-id/index.ts b/packages/ui/src/components/organisms/Form/Validator/utils/format-id/index.ts new file mode 100644 index 0000000000..64ae441983 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/format-id/index.ts @@ -0,0 +1 @@ +export * from './format-id'; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.ts b/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.ts new file mode 100644 index 0000000000..d7f265866c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.ts @@ -0,0 +1,11 @@ +import { TDeepthLevelStack } from '../../types'; + +export const formatValueDestination = (valueDestination: string, stack: TDeepthLevelStack) => { + let _valueDestination = valueDestination; + + stack.forEach((stack, index) => { + _valueDestination = _valueDestination.replace(`$${index}`, stack.toString()); + }); + + return _valueDestination; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.unit.test.ts new file mode 100644 index 0000000000..5471e6b80e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.unit.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import { formatValueDestination } from './format-value-destination'; + +describe('formatValueDestination', () => { + it('should be defined', () => { + expect(formatValueDestination).toBeDefined(); + }); + + describe('formatting', () => { + it('should format simple value destination', () => { + const valueDestination = 'tasks[$0].name'; + const stack = [1]; + + expect(formatValueDestination(valueDestination, stack)).toBe('tasks[1].name'); + }); + + it('should format nested value destination', () => { + const valueDestination = 'tasks[$0].siblings[$1].name'; + const stack = [1, 2]; + + expect(formatValueDestination(valueDestination, stack)).toBe('tasks[1].siblings[2].name'); + }); + + it('should handle empty stack', () => { + const valueDestination = 'tasks.name'; + const stack: number[] = []; + + expect(formatValueDestination(valueDestination, stack)).toBe('tasks.name'); + }); + + it('should handle value destination without placeholders', () => { + const valueDestination = 'tasks[0].siblings[1].name'; + const stack = [1, 2]; + + expect(formatValueDestination(valueDestination, stack)).toBe('tasks[0].siblings[1].name'); + }); + + it('should replace placeholder with index', () => { + const valueDestination = 'tasks[$0].siblings[$1].name'; + const stack = [1]; + + expect(formatValueDestination(valueDestination, stack)).toBe('tasks[1].siblings[$1].name'); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/index.ts b/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/index.ts new file mode 100644 index 0000000000..f4a6d4a9ff --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/index.ts @@ -0,0 +1 @@ +export * from './format-value-destination'; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/validate/types.ts b/packages/ui/src/components/organisms/Form/Validator/utils/validate/types.ts new file mode 100644 index 0000000000..5efd730a3f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/validate/types.ts @@ -0,0 +1,3 @@ +export interface IValidateParams { + abortEarly?: boolean; +} diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts index e69de29bb2..9020c79368 100644 --- a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts +++ b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts @@ -0,0 +1,69 @@ +import get from 'lodash/get'; +import { IValidationError, IValidationSchema } from '../../types'; +import { createValidationError } from '../create-validation-error'; +import { formatValueDestination } from '../format-value-destination'; +import { getValidator } from '../get-validator'; +import { IValidateParams } from './types'; + +// TODO: Codnitional Apply +// TODO: Test coverage ror custom validators + +export const validate = ( + context: TValues, + schema: IValidationSchema[], + params: IValidateParams = {}, +): IValidationError[] => { + const { abortEarly = false } = params; + + const validationErrors: IValidationError[] = []; + + const run = (schema: IValidationSchema[], stack: number[] = []) => { + schema.forEach(schema => { + const { validators = [], children, valueDestination, id } = schema; + const formattedValueDestination = valueDestination + ? formatValueDestination(valueDestination, stack) + : ''; + + const value = formattedValueDestination ? get(context, formattedValueDestination) : context; + + for (const validator of validators) { + const validate = getValidator(validator); + + try { + validate(value, validator); + } catch (exception) { + const error = createValidationError({ + id, + invalidValue: value, + message: (exception as Error).message, + stack, + }); + + validationErrors.push(error); + + if (abortEarly) { + throw validationErrors; + } + } + } + + if (children?.length && Array.isArray(value)) { + value.forEach((_, index) => { + run(children, [...stack, index]); + }); + } + }); + }; + + try { + run(schema); + } catch (exception) { + if (exception instanceof Error) { + throw exception; + } + + return validationErrors; + } + + return validationErrors; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts new file mode 100644 index 0000000000..6dae7506c5 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts @@ -0,0 +1,730 @@ +import { describe, expect, it, test } from 'vitest'; +import { IValidationError, IValidationSchema } from '../../types'; +import { validate } from './validate'; + +describe('validate', () => { + it('should be defined', () => { + expect(validate).toBeDefined(); + }); + + describe('validation', () => { + describe('abort early', () => { + it('should return only first error when abortEarly is true', () => { + const testValue = { + name: null, + age: 25, + }; + + const schema = [ + { + id: 'name', + valueDestination: 'name', + validators: [{ type: 'required', message: 'Name is required.', value: {} }], + }, + { + id: 'age', + valueDestination: 'age', + validators: [ + { + type: 'maximum', + message: 'Age must be 20 or less', + value: { maximum: 20 }, + }, + ], + }, + ] as IValidationSchema[]; + + const errors = validate(testValue, schema, { abortEarly: true }); + + expect(errors.length).toBe(1); + expect(errors?.[0]?.message).toEqual(['Name is required.']); + }); + + it('should return all errors when abortEarly is false', () => { + const testValue = { + name: null, + age: 25, + }; + + const schema = [ + { + id: 'name', + valueDestination: 'name', + validators: [{ type: 'required', message: 'Name is required.', value: {} }], + }, + { + id: 'age', + valueDestination: 'age', + validators: [ + { + type: 'maximum', + message: 'Age must be 20 or less', + value: { maximum: 20 }, + }, + ], + }, + ] as IValidationSchema[]; + + const errors = validate(testValue, schema, { abortEarly: false }); + + expect(errors.length).toBe(2); + expect(errors?.[0]?.message).toEqual(['Name is required.']); + expect(errors?.[1]?.message).toEqual(['Age must be 20 or less']); + }); + }); + + describe('plain objects', () => { + describe('will be valid', () => { + const requiredCase = [ + { + name: 'John', + }, + [ + { + id: 'name', + valueDestination: 'name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + ] as IValidationSchema[], + ] as const; + + const maximumValueCase = [ + { + age: 20, + }, + [ + { + id: 'age', + valueDestination: 'age', + validators: [ + { + type: 'maximum', + message: 'Maximum value is {maximum}', + value: { + maximum: 20, + }, + }, + ], + }, + ] as IValidationSchema[], + ] as const; + + const minimumValueCase = [ + { + age: 20, + }, + [ + { + id: 'age', + valueDestination: 'age', + validators: [ + { + type: 'minimum', + message: 'Minimum value is {minimum}', + value: { + minimum: 20, + }, + }, + ], + }, + ] as IValidationSchema[], + ] as const; + + const maxLengthStringCase = [ + { + name: 'John', + }, + [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { type: 'maxLength', message: 'Field is invalid.', value: { maxLength: 4 } }, + ], + }, + ] as IValidationSchema[], + ] as const; + + const maxLengthArrayCase = [ + { + list: [1, 2, 3, 4], + }, + [ + { + id: 'list', + valueDestination: 'list', + validators: [ + { type: 'maxLength', message: 'Field is invalid.', value: { maxLength: 4 } }, + ], + }, + ] as IValidationSchema[], + ] as const; + + const minLengthArrayCase = [ + { + list: [1, 2, 3, 4], + }, + [ + { + id: 'list', + valueDestination: 'list', + validators: [ + { type: 'minLength', message: 'Field is invalid.', value: { minLength: 4 } }, + ], + }, + ] as IValidationSchema[], + ] as const; + + const minLengthStringCase = [ + { + name: 'John', + }, + [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { type: 'minLength', message: 'Field is invalid.', value: { minLength: 4 } }, + ], + }, + ] as IValidationSchema[], + ] as const; + + const patternStringCase = [ + { + name: 'John', + }, + [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { type: 'pattern', message: 'Field is invalid.', value: { pattern: /[A-Z]/ } }, + ], + }, + ] as IValidationSchema[], + ] as const; + + const cases = [ + requiredCase, + maximumValueCase, + minimumValueCase, + maxLengthStringCase, + maxLengthArrayCase, + minLengthArrayCase, + minLengthStringCase, + patternStringCase, + ]; + + test.each(cases)('is valid', (testData, schema) => { + const errors = validate(testData, schema); + + expect(errors).toEqual([]); + }); + }); + + describe('will be invalid', () => { + const requiredCase = [ + { + name: null, + }, + [ + { + id: 'name', + valueDestination: 'name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + ] as IValidationSchema[], + ['Field is required.'], + ] as const; + + const maximumValueCase = [ + { + age: 25, + }, + [ + { + id: 'age', + valueDestination: 'age', + validators: [ + { + type: 'maximum', + message: 'Maximum value is {maximum}', + value: { + maximum: 20, + }, + }, + ], + }, + ] as IValidationSchema[], + ['Maximum value is 20'], + ] as const; + + const minimumValueCase = [ + { + age: 15, + }, + [ + { + id: 'age', + valueDestination: 'age', + validators: [ + { + type: 'minimum', + message: 'Minimum value is {minimum}', + value: { + minimum: 20, + }, + }, + ], + }, + ] as IValidationSchema[], + ['Minimum value is 20'], + ] as const; + + const maxLengthStringCase = [ + { + name: 'John Doe', + }, + [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { type: 'maxLength', message: 'Field is invalid.', value: { maxLength: 4 } }, + ], + }, + ] as IValidationSchema[], + ['Field is invalid.'], + ] as const; + + const maxLengthArrayCase = [ + { + list: [1, 2, 3, 4, 5], + }, + [ + { + id: 'list', + valueDestination: 'list', + validators: [ + { type: 'maxLength', message: 'Field is invalid.', value: { maxLength: 4 } }, + ], + }, + ] as IValidationSchema[], + ['Field is invalid.'], + ] as const; + + const minLengthArrayCase = [ + { + list: [1, 2, 3], + }, + [ + { + id: 'list', + valueDestination: 'list', + validators: [ + { type: 'minLength', message: 'Field is invalid.', value: { minLength: 4 } }, + ], + }, + ] as IValidationSchema[], + ['Field is invalid.'], + ] as const; + + const minLengthStringCase = [ + { + name: 'Doe', + }, + [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { type: 'minLength', message: 'Field is invalid.', value: { minLength: 4 } }, + ], + }, + ] as IValidationSchema[], + ['Field is invalid.'], + ] as const; + + const patternStringCase = [ + { + name: '1', + }, + [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { type: 'pattern', message: 'Field is invalid.', value: { pattern: /[A-Z]/ } }, + ], + }, + ] as IValidationSchema[], + ['Field is invalid.'], + ] as const; + + const cases = [ + requiredCase, + maximumValueCase, + minimumValueCase, + maxLengthStringCase, + maxLengthArrayCase, + minLengthArrayCase, + minLengthStringCase, + patternStringCase, + ]; + + test.each(cases)('validation will fail', (testData, schema, expectedErrors) => { + const errors = validate(testData, schema); + const error = errors[0]; + + expect(errors.length).toEqual(1); + expect(error?.message[0]).toEqual(expectedErrors[0]); + }); + }); + }); + + describe('nested objects', () => { + it('will be valid', () => { + const testValue = { + name: 'John', + tasks: [ + { + name: 'Jane', + }, + { + name: 'Jim', + siblings: [ + { + name: 'Jill', + }, + ], + }, + ], + }; + + const schema = [ + { + id: 'name', + valueDestination: 'name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + { + id: 'tasks', + valueDestination: 'tasks', + validators: [ + { + type: 'minLength', + message: 'Field is invalid.', + value: { minLength: 2 }, + }, + ], + children: [ + { + id: 'tasksName', + valueDestination: 'tasks[$0].name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + { + id: 'siblings', + valueDestination: 'tasks[$0].siblings', + children: [ + { + id: 'siblingsName', + valueDestination: 'tasks[$0].siblings[$1].name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + ], + }, + ], + }, + ] as IValidationSchema[]; + + expect(validate(testValue, schema)).toEqual([]); + }); + + it('will be invalid', () => { + const testValue = { + name: 'John', + tasks: [ + { + name: 'Jane', + }, + { + name: 'Jim', + siblings: [ + { + name: 'Jill', + }, + ], + }, + ], + }; + + const schema = [ + { + id: 'name', + valueDestination: 'name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + { + id: 'tasks', + valueDestination: 'tasks', + validators: [ + { + type: 'minLength', + message: 'Field is invalid.', + value: { minLength: 2 }, + }, + ], + children: [ + { + id: 'tasksName', + valueDestination: 'tasks[$0].name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + { + id: 'siblings', + valueDestination: 'tasks[$0].siblings', + children: [ + { + id: 'siblingsName', + valueDestination: 'tasks[$0].siblings[$1].name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + ], + }, + ], + }, + ] as IValidationSchema[]; + + expect(validate(testValue, schema)).toEqual([]); + }); + }); + + describe('when validating array entries as root', () => { + it('will be valid', () => { + const value = [ + { + name: 'John Doe', + }, + ]; + + const schema = [ + { + id: 'list', + children: [ + { + id: 'name', + valueDestination: '[$0].name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + ], + }, + ] as IValidationSchema[]; + + expect(validate(value, schema)).toEqual([]); + }); + + it('will be invalid', () => { + const value = [ + { + name: null, + }, + ]; + + const schema = [ + { + id: 'list', + children: [ + { + id: 'name', + valueDestination: '[$0].name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + ], + }, + ] as IValidationSchema[]; + + const errors = validate(value, schema); + + expect(errors.length).toBe(1); + expect(errors[0]?.message[0]).toEqual('Field is required.'); + }); + }); + + describe('validation errors', () => { + it('should be returned as array', () => { + const testValue = { + name: null, + }; + + const schema = [ + { + id: 'name', + valueDestination: 'name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + ] as IValidationSchema[]; + + expect(validate(testValue, schema)).toEqual([ + { + id: 'name', + originId: 'name', + invalidValue: null, + message: ['Field is required.'], + }, + ]); + }); + + describe('with formattedId', () => { + const oneLevelDepthCase = [ + { + items: [ + { + name: null, + }, + ], + }, + [ + { + id: 'items', + valueDestination: 'items', + children: [ + { + id: 'name', + valueDestination: 'items[$0].name', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + ], + }, + ] as IValidationSchema[], + { + id: 'name-0', + originId: 'name', + invalidValue: null, + message: ['Field is required.'], + } as IValidationError, + ] as const; + + const twoLevelDepthCase = [ + { + items: [ + { + name: null, + subItems: [ + { + subName: null, + }, + ], + }, + ], + }, + [ + { + id: 'items', + valueDestination: 'items', + children: [ + { + id: 'name', + valueDestination: 'items[$0].subItems', + children: [ + { + id: 'subName', + valueDestination: 'items[$0].subItems[$1].subName', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + ], + }, + ], + }, + ] as IValidationSchema[], + { + id: 'subName-0-0', + originId: 'subName', + invalidValue: null, + message: ['Field is required.'], + } as IValidationError, + ] as const; + + test.each([oneLevelDepthCase, twoLevelDepthCase])( + 'should return errors with formattedId', + (testData, schema, expectedErrors) => { + const errors = validate(testData, schema); + const error = errors[0]; + + expect(errors?.length).toBe(1); + expect(error).toEqual(expectedErrors); + }, + ); + }); + + describe('nested arrays with multiple items', () => { + it('will be valid', () => { + const value = { + items: [ + { + name: null, + subItems: [ + { + subName: null, + }, + { + subName: null, + }, + ], + }, + { + subItems: [ + { + subName: null, + }, + ], + }, + ], + }; + + const schema = [ + { + id: 'items', + valueDestination: 'items', + children: [ + { + id: 'name', + valueDestination: 'items[$0].subItems', + children: [ + { + id: 'subName', + valueDestination: 'items[$0].subItems[$1].subName', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + ], + }, + ], + }, + ] as IValidationSchema[]; + + expect(validate(value, schema)).toEqual([ + { + id: 'subName-0-0', + originId: 'subName', + invalidValue: null, + message: ['Field is required.'], + }, + { + id: 'subName-0-1', + originId: 'subName', + invalidValue: null, + message: ['Field is required.'], + }, + { + id: 'subName-1-0', + originId: 'subName', + invalidValue: null, + message: ['Field is required.'], + }, + ]); + }); + }); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/format/format-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/format/format-validator.ts index f0a34274a9..b8dbc64690 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/format/format-validator.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/format/format-validator.ts @@ -7,6 +7,8 @@ export const formatValidator: TValidator = value, params, ) => { + if (typeof value !== 'string') return true; + const { message = 'Invalid {format} format.' } = params; if (params.value.format === 'email') { diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/format/format.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/format/format.validator.unit.test.ts index 9c083d3983..9bf1f1fdd2 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/format/format.validator.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/format/format.validator.unit.test.ts @@ -30,10 +30,8 @@ describe('formatValidator', () => { ); }); - it('should throw error for non-string value', () => { - expect(() => formatValidator(123, params as ICommonValidator)).toThrow( - 'Invalid email format.', - ); + it('should return true for non-string value', () => { + expect(formatValidator(123, params as ICommonValidator)).toBe(true); }); }); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length-validator.ts index 824566ba61..abf7d21c5a 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length-validator.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length-validator.ts @@ -8,7 +8,11 @@ export const maxLengthValidator: TValidator { const { message = 'Maximum length is {maxLength}.' } = params; + if (value?.length === undefined) return true; + if (value?.length > params.value.maxLength) { throw new Error(formatErrorMessage(message, 'maxLength', params.value.maxLength.toString())); } + + return true; }; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length.validator.unit.test.ts index 3c6880d4ac..321d37803b 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length.validator.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/max-length/max-length.validator.unit.test.ts @@ -7,6 +7,10 @@ describe('maxLengthValidator', () => { value: { maxLength: 5 }, }; + it('should return true for non-string and non-array value', () => { + expect(maxLengthValidator(123 as any, params as ICommonValidator)).toBe(true); + }); + it('should not throw error when string length is equal to maxLength', () => { expect(() => maxLengthValidator('12345', params as ICommonValidator)).not.toThrow(); }); @@ -32,13 +36,7 @@ describe('maxLengthValidator', () => { ); }); - it('should handle empty string', () => { - expect(() => maxLengthValidator('', params as ICommonValidator)).not.toThrow(); - }); - - it('should handle undefined value', () => { - expect(() => - maxLengthValidator(undefined as any, params as ICommonValidator), - ).not.toThrow(); + it('should return true for undefined value', () => { + expect(maxLengthValidator(undefined as any, params as ICommonValidator)).toBe(true); }); }); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum-validator.ts index 31b1716b59..9ee7850ed9 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum-validator.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum-validator.ts @@ -8,6 +8,8 @@ export const maximumValueValidator: TValidator { const { message = 'Maximum value is {maximum}.' } = params; + if (typeof value !== 'number') return true; + if (value > params.value.maximum) { throw new Error(formatErrorMessage(message, 'maximum', params.value.maximum.toString())); } diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum.validator.unit.test.ts index 771c421eab..1cab5b9ad3 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum.validator.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/maximum/maximum.validator.unit.test.ts @@ -38,4 +38,10 @@ describe('maximumValueValidator', () => { ); expect(() => maximumValueValidator(9.9, params as ICommonValidator)).not.toThrow(); }); + + it('should return true for non-number values', () => { + expect(maximumValueValidator('test' as any, params as ICommonValidator)).toBe(true); + expect(maximumValueValidator(undefined as any, params as ICommonValidator)).toBe(true); + expect(maximumValueValidator(null as any, params as ICommonValidator)).toBe(true); + }); }); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length-validator.ts index 5749e4c004..6faf36f7ba 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length-validator.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length-validator.ts @@ -8,7 +8,9 @@ export const minLengthValidator: TValidator { const { message = 'Minimum length is {minLength}.' } = params; - if (value === undefined || value?.length < params.value.minLength) { + if (value?.length === undefined) return true; + + if (value?.length < params.value.minLength) { throw new Error(formatErrorMessage(message, 'minLength', params.value.minLength.toString())); } }; diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length.validator.unit.test.ts index 25d7920637..5db8bfb98b 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length.validator.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length.validator.unit.test.ts @@ -38,9 +38,7 @@ describe('minLengthValidator', () => { ); }); - it('should handle undefined value', () => { - expect(() => minLengthValidator(undefined as any, params as ICommonValidator)).toThrow( - 'Minimum length is 4.', - ); + it('should return true for undefined value', () => { + expect(minLengthValidator(undefined as any, params as ICommonValidator)).toBe(true); }); }); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum-value-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum-value-validator.ts index e33747bdef..088aaac355 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum-value-validator.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum-value-validator.ts @@ -8,6 +8,8 @@ export const minimumValueValidator: TValidator { const { message = 'Minimum value is {minimum}.' } = params; + if (typeof value !== 'number') return true; + if (value < params.value.minimum) { throw new Error(formatErrorMessage(message, 'minimum', params.value.minimum.toString())); } diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum.validator.unit.test.ts index 4aad13ac10..7699d20f6f 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum.validator.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/minimum/minimum.validator.unit.test.ts @@ -31,4 +31,17 @@ describe('minimumValueValidator', () => { 'Custom message: min 5', ); }); + + it('should handle decimal values', () => { + expect(() => minimumValueValidator(4.9, params as ICommonValidator)).toThrow( + 'Minimum value is 5.', + ); + expect(() => minimumValueValidator(5.1, params as ICommonValidator)).not.toThrow(); + }); + + it('should return true for non-number values', () => { + expect(minimumValueValidator('test' as any, params as ICommonValidator)).toBe(true); + expect(minimumValueValidator(undefined as any, params as ICommonValidator)).toBe(true); + expect(minimumValueValidator(null as any, params as ICommonValidator)).toBe(true); + }); }); diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern-validator.ts b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern-validator.ts index 19c46ec713..7125214799 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern-validator.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern-validator.ts @@ -8,6 +8,8 @@ export const patternValueValidator: TValidator ) => { const { message = `Value must match {pattern}.` } = params; + if (typeof value !== 'string') return true; + if (!new RegExp(params.value.pattern).test(value as string)) { throw new Error(formatErrorMessage(message, 'pattern', params.value.pattern)); } diff --git a/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern.validator.unit.test.ts index 643e122919..a062dcdf95 100644 --- a/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern.validator.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/validators/pattern/pattern.validator.unit.test.ts @@ -34,9 +34,9 @@ describe('patternValueValidator', () => { ); }); - it('should handle undefined value', () => { - expect(() => patternValueValidator(undefined as any, params as ICommonValidator)).toThrow( - 'Value must match ^[A-Z]+$.', - ); + it('should return true for non-string values', () => { + expect(patternValueValidator(undefined as any, params as ICommonValidator)).toBe(true); + expect(patternValueValidator(null as any, params as ICommonValidator)).toBe(true); + expect(patternValueValidator(123 as any, params as ICommonValidator)).toBe(true); }); }); From e471f8122d6dd229a0c2e50ef3a557a63d1457b4 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Tue, 3 Dec 2024 17:16:49 +0200 Subject: [PATCH 03/54] fix: test --- .../create-validation-error.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.unit.test.ts index 4a12fe744d..3be85c8eea 100644 --- a/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/utils/create-validation-error/create-validation-error.unit.test.ts @@ -31,7 +31,7 @@ describe('createValidationError', () => { const result = createValidationError(params); expect(result).toEqual({ - id: 'test-', + id: 'test', originId: 'test', invalidValue: 123, message: ['error message'], From 7cb51fe1bf5ac6cd7ee1cb42cced56266f19e55a Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Wed, 4 Dec 2024 13:28:23 +0200 Subject: [PATCH 04/54] feat: implemented conditional validation rule apply & custom validators --- packages/ui/package.json | 2 + .../organisms/Form/Validator/types/index.ts | 9 +- .../utils/get-validator/get-validator.ts | 10 +- .../Validator/utils/remove-validator/index.ts | 1 + .../remove-validator/remove-validator.ts | 5 + .../remove-validator.unit.test.ts | 51 +++++++ .../Form/Validator/utils/validate/helpers.ts | 6 + .../Form/Validator/utils/validate/validate.ts | 28 ++-- .../utils/validate/validate.unit.test.ts | 134 +++++++++++++++++- pnpm-lock.yaml | 6 + .../workflows-service/prisma/data-migrations | 2 +- 11 files changed, 236 insertions(+), 18 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/remove-validator.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/remove-validator.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/utils/validate/helpers.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index 8aa5a22c86..3891a490ad 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -53,6 +53,7 @@ "dayjs": "^1.11.6", "email-validator": "^2.0.4", "i18n-iso-countries": "^7.6.0", + "json-logic-js": "^2.0.2", "lodash": "^4.17.21", "lucide-react": "^0.144.0", "react": "^18.0.37", @@ -79,6 +80,7 @@ "@storybook/testing-library": "^0.0.14-next.2", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^13.3.0", + "@types/json-logic-js": "^2.0.1", "@types/lodash": "^4.14.191", "@types/node": "^20.4.1", "@types/react": "^18.0.37", diff --git a/packages/ui/src/components/organisms/Form/Validator/types/index.ts b/packages/ui/src/components/organisms/Form/Validator/types/index.ts index 2ff685523b..9de2abff53 100644 --- a/packages/ui/src/components/organisms/Form/Validator/types/index.ts +++ b/packages/ui/src/components/organisms/Form/Validator/types/index.ts @@ -13,17 +13,20 @@ export type TBaseValidators = | 'minimum' | 'maximum'; -export interface ICommonValidator { +export interface ICommonValidator { type: TValidatorType; value: T; message?: string; applyWhen?: IValidationRule; } -export interface IValidationSchema { +export interface IValidationSchema< + TValidatorTypeExtends extends string = TBaseValidators, + TValue = object, +> { id: string; valueDestination?: string; - validators: ICommonValidator[]; + validators: Array>; children?: IValidationSchema[]; } diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.ts b/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.ts index 5ce8a0d309..dab1590fe3 100644 --- a/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.ts +++ b/packages/ui/src/components/organisms/Form/Validator/utils/get-validator/get-validator.ts @@ -1,8 +1,12 @@ -import { ICommonValidator } from '../../types'; +import { ICommonValidator, TBaseValidators } from '../../types'; import { baseValidatorsMap, validatorsExtends } from '../../validators'; -export const getValidator = (validator: ICommonValidator) => { - const validatorFn = baseValidatorsMap[validator.type] || validatorsExtends[validator.type]; +export const getValidator = ( + validator: ICommonValidator, +) => { + const validatorFn = + baseValidatorsMap[validator.type as unknown as TBaseValidators] || + validatorsExtends[validator.type]; if (!validatorFn) { throw new Error(`Validator ${validator.type} not found.`); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/index.ts b/packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/index.ts new file mode 100644 index 0000000000..6b125ff849 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/index.ts @@ -0,0 +1 @@ +export * from './remove-validator'; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/remove-validator.ts b/packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/remove-validator.ts new file mode 100644 index 0000000000..60b72cc6f4 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/remove-validator.ts @@ -0,0 +1,5 @@ +import { validatorsExtends } from '../../validators'; + +export const removeValidator = (type: string) => { + delete validatorsExtends[type]; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/remove-validator.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/remove-validator.unit.test.ts new file mode 100644 index 0000000000..87d1261999 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/remove-validator/remove-validator.unit.test.ts @@ -0,0 +1,51 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { removeValidator } from './remove-validator'; + +vi.mock('../../validators', () => ({ + validatorsExtends: {}, +})); + +describe('removeValidator', async () => { + const validatorsExtends = vi.mocked(await import('../../validators')).validatorsExtends; + + beforeEach(() => { + // Clear validators before each test + Object.keys(validatorsExtends).forEach(key => { + delete validatorsExtends[key]; + }); + }); + + it('should remove validator from validatorsExtends', () => { + // Setup + const mockValidator = vi.fn(); + validatorsExtends['test'] = mockValidator; + expect(validatorsExtends['test']).toBe(mockValidator); + + // Execute + removeValidator('test'); + + // Verify + expect(validatorsExtends['test']).toBeUndefined(); + }); + + it('should not throw error when removing non-existent validator', () => { + expect(() => { + removeValidator('nonexistent'); + }).not.toThrow(); + }); + + it('should only remove specified validator', () => { + // Setup + const mockValidator1 = vi.fn(); + const mockValidator2 = vi.fn(); + validatorsExtends['test1'] = mockValidator1; + validatorsExtends['test2'] = mockValidator2; + + // Execute + removeValidator('test1'); + + // Verify + expect(validatorsExtends['test1']).toBeUndefined(); + expect(validatorsExtends['test2']).toBe(mockValidator2); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/validate/helpers.ts b/packages/ui/src/components/organisms/Form/Validator/utils/validate/helpers.ts new file mode 100644 index 0000000000..5d5030b7e1 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/utils/validate/helpers.ts @@ -0,0 +1,6 @@ +import jsonLogic from 'json-logic-js'; +import { IValidationRule } from '../../types'; + +export const isShouldApplyValidation = (rule: IValidationRule, context: object) => { + return Boolean(jsonLogic.apply(rule.value, context)); +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts index 9020c79368..77b55bbd31 100644 --- a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts +++ b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts @@ -1,23 +1,29 @@ import get from 'lodash/get'; -import { IValidationError, IValidationSchema } from '../../types'; +import { + ICommonValidator, + IValidationError, + IValidationSchema, + TBaseValidators, +} from '../../types'; import { createValidationError } from '../create-validation-error'; import { formatValueDestination } from '../format-value-destination'; import { getValidator } from '../get-validator'; +import { isShouldApplyValidation } from './helpers'; import { IValidateParams } from './types'; -// TODO: Codnitional Apply -// TODO: Test coverage ror custom validators - -export const validate = ( +export const validate = < + TValues extends object, + TValidatorTypeExtends extends string = TBaseValidators, +>( context: TValues, - schema: IValidationSchema[], + schema: Array>, params: IValidateParams = {}, ): IValidationError[] => { const { abortEarly = false } = params; const validationErrors: IValidationError[] = []; - const run = (schema: IValidationSchema[], stack: number[] = []) => { + const run = (schema: Array>, stack: number[] = []) => { schema.forEach(schema => { const { validators = [], children, valueDestination, id } = schema; const formattedValueDestination = valueDestination @@ -27,10 +33,14 @@ export const validate = ( const value = formattedValueDestination ? get(context, formattedValueDestination) : context; for (const validator of validators) { + if (validator.applyWhen && !isShouldApplyValidation(validator.applyWhen, context)) { + continue; + } + const validate = getValidator(validator); try { - validate(value, validator); + validate(value, validator as unknown as ICommonValidator); } catch (exception) { const error = createValidationError({ id, @@ -49,7 +59,7 @@ export const validate = ( if (children?.length && Array.isArray(value)) { value.forEach((_, index) => { - run(children, [...stack, index]); + run(children as Array>, [...stack, index]); }); } }); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts index 6dae7506c5..d338ef4fa1 100644 --- a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts @@ -1,5 +1,6 @@ -import { describe, expect, it, test } from 'vitest'; -import { IValidationError, IValidationSchema } from '../../types'; +import { beforeEach, describe, expect, it, test, vi } from 'vitest'; +import { ICommonValidator, IValidationError, IValidationSchema } from '../../types'; +import { registerValidator } from '../register-validator'; import { validate } from './validate'; describe('validate', () => { @@ -726,5 +727,134 @@ describe('validate', () => { }); }); }); + + describe('conditional validation', () => { + const case1 = [ + { + firstName: 'John', + lastName: undefined, + }, + [ + { + id: 'name', + valueDestination: 'firstName', + validators: [{ type: 'required', message: 'Field is required.', value: {} }], + }, + { + id: 'lastName', + valueDestination: 'lastName', + validators: [ + { + type: 'required', + message: 'Field is required.', + value: {}, + applyWhen: { + type: 'json-logic', + value: { + var: 'firstName', + }, + }, + }, + ], + }, + ] as IValidationSchema[], + { + id: 'lastName', + originId: 'lastName', + invalidValue: undefined, + message: ['Field is required.'], + } as IValidationError, + ] as const; + + const case2 = [ + { + firstName: 'Banana', + lastName: undefined, + }, + [ + { + id: 'lastName', + valueDestination: 'lastName', + validators: [ + { + type: 'required', + message: 'Field is required.', + value: {}, + applyWhen: { + type: 'json-logic', + value: { + '==': [{ var: 'firstName' }, 'Banana'], + }, + }, + }, + ], + }, + ] as IValidationSchema[], + { + id: 'lastName', + originId: 'lastName', + invalidValue: undefined, + message: ['Field is required.'], + } as IValidationError, + ] as const; + + const cases = [case1, case2]; + + test.each(cases)( + 'should be applied when the condition is truthy', + (testData, schema, expectedErrors) => { + const errors = validate(testData, schema); + + expect(errors).toEqual([expectedErrors]); + }, + ); + }); + + describe('custom validators', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('even number validator', () => { + const evenNumberValidator = (value: number, _: ICommonValidator) => { + if (typeof value !== 'number') return true; + + if (value % 2 !== 0) { + throw new Error('Number is not even'); + } + }; + + registerValidator('evenNumber', evenNumberValidator); + + const data = { + odd: 19, + even: 20, + }; + + const schema = [ + { + id: 'odd', + valueDestination: 'odd', + validators: [{ type: 'evenNumber', value: {} }], + }, + { + id: 'even', + valueDestination: 'even', + validators: [{ type: 'evenNumber', value: {} }], + }, + ] as Array>; + + const errors = validate(data, schema); + + expect(errors).toEqual([ + { + id: 'odd', + originId: 'odd', + invalidValue: 19, + message: ['Number is not even'], + }, + ]); + }); + }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e72a691f6..d9e5c2c26e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1802,6 +1802,9 @@ importers: i18n-iso-countries: specifier: ^7.6.0 version: 7.7.0 + json-logic-js: + specifier: ^2.0.2 + version: 2.0.2 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -1875,6 +1878,9 @@ importers: '@testing-library/react': specifier: ^13.3.0 version: 13.4.0(react-dom@18.2.0)(react@18.2.0) + '@types/json-logic-js': + specifier: ^2.0.1 + version: 2.0.5 '@types/lodash': specifier: ^4.14.191 version: 4.14.201 diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations index 79ee883a56..7c197ae8dc 160000 --- a/services/workflows-service/prisma/data-migrations +++ b/services/workflows-service/prisma/data-migrations @@ -1 +1 @@ -Subproject commit 79ee883a5606b2dc562ac8530bb493e2e23faadd +Subproject commit 7c197ae8dc1d459e4049ad01d86cfb4e7029bb53 From 4d2c7086e143059223119a4b065b920448095a0f Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Wed, 4 Dec 2024 17:50:15 +0200 Subject: [PATCH 05/54] feat: implemented useValidate & tests --- .../Form/Validator/ValidatorProvider.tsx | 1 + .../hooks/internal/useValidate/index.ts | 1 + .../internal/useValidate/useAsyncValidate.ts | 46 ++ .../useValidate/useAsyncValidate.unit.test.ts | 111 ++++ .../internal/useValidate/useManualValidate.ts | 24 + .../useManualValidate.unit.test.ts | 93 ++++ .../internal/useValidate/useSyncValidate.ts | 23 + .../useValidate/useSyncValidate.unit.test.ts | 108 ++++ .../hooks/internal/useValidate/useValidate.ts | 59 ++ .../useValidate/useValidate.unit.test.ts | 525 ++++++++++++++++++ 10 files changed, 991 insertions(+) create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.ts create 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/hooks/internal/useValidate/useValidate.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts diff --git a/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx b/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx index 3d78aceb8a..4cb4397fa5 100644 --- a/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx +++ b/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx @@ -9,6 +9,7 @@ export interface IValidatorProviderProps { ref?: React.RefObject; validateOnChange?: boolean; + validateSync?: boolean; } export const ValidatorProvider = ({ diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/index.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/index.ts new file mode 100644 index 0000000000..7ea5d57fe6 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/index.ts @@ -0,0 +1 @@ +export * from './useValidate'; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.ts new file mode 100644 index 0000000000..5dba2f72c0 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.ts @@ -0,0 +1,46 @@ +import { IValidationSchema } from '../../../types'; + +import debounce from 'lodash/debounce'; +import { useCallback, useEffect, useState } from 'react'; +import { IValidationError } from '../../../types'; +import { validate } from '../../../utils/validate'; + +export interface IUseAsyncValidateParams { + validationDelay?: number; + validateAsync?: boolean; + validateOnChange?: boolean; + abortEarly?: boolean; +} + +export const useAsyncValidate = ( + context: object, + schema: IValidationSchema[], + params: IUseAsyncValidateParams = {}, +) => { + const { + validationDelay = 500, + validateAsync = false, + validateOnChange = true, + abortEarly = false, + } = params; + + const [validationErrors, setValidationErrors] = useState(() => + validateAsync ? validate(context, schema, { abortEarly }) : [], + ); + + const validateWithDebounce = useCallback( + debounce((context: object, schema: IValidationSchema[], params: IUseAsyncValidateParams) => { + const errors = validate(context, schema, params); + setValidationErrors(errors); + }, validationDelay), + [validationDelay], + ); + + useEffect(() => { + if (!validateAsync || !validateOnChange) return; + + validateWithDebounce(context, schema, { abortEarly }); + }, [context, schema, validateAsync, validateOnChange, abortEarly, validateWithDebounce]); + + return validationErrors; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.unit.test.ts new file mode 100644 index 0000000000..c7bdaffdcf --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.unit.test.ts @@ -0,0 +1,111 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { validate } from '../../../utils/validate'; +import { useAsyncValidate } from './useAsyncValidate'; + +// Mock dependencies +vi.mock('../../../utils/validate', () => ({ + validate: vi.fn().mockReturnValue([ + { + id: 'name', + originId: 'name', + message: ['error'], + invalidValue: 'John', + }, + ]), +})); + +vi.mock('lodash/debounce', () => ({ + default: (fn: any) => fn, +})); + +describe('useAsyncValidate', () => { + const mockContext = { name: 'John' }; + const mockSchema = [{ id: 'name', validators: [], rules: [] }]; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should initialize with empty validation errors', () => { + const { result } = renderHook(() => useAsyncValidate(mockContext, mockSchema)); + expect(result.current).toEqual([]); + }); + + it('should not validate when validateAsync is false', () => { + renderHook(() => useAsyncValidate(mockContext, mockSchema, { validateAsync: false })); + expect(validate).not.toHaveBeenCalled(); + }); + + it('should not validate when validateOnChange is false', () => { + renderHook(() => useAsyncValidate(mockContext, mockSchema, { validateOnChange: false })); + expect(validate).not.toHaveBeenCalled(); + }); + + it('should validate and set errors when validateAsync and validateOnChange are true', () => { + const { result } = renderHook(() => + useAsyncValidate(mockContext, mockSchema, { + validateAsync: true, + validateOnChange: true, + }), + ); + + expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: false }); + expect(result.current).toEqual([ + { + id: 'name', + originId: 'name', + message: ['error'], + invalidValue: 'John', + }, + ]); + }); + + it('should pass abortEarly param to validate function', () => { + renderHook(() => + useAsyncValidate(mockContext, mockSchema, { + validateAsync: true, + validateOnChange: true, + abortEarly: true, + }), + ); + + expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: true }); + }); + + it('should revalidate when context changes', () => { + const { rerender } = renderHook( + ({ context }) => + useAsyncValidate(context, mockSchema, { + validateAsync: true, + validateOnChange: true, + }), + { + initialProps: { context: mockContext }, + }, + ); + + const newContext = { name: 'Jane' }; + rerender({ context: newContext }); + + expect(validate).toHaveBeenCalledWith(newContext, mockSchema, { abortEarly: false }); + }); + + it('should revalidate when schema changes', () => { + const { rerender } = renderHook( + ({ schema }) => + useAsyncValidate(mockContext, schema, { + validateAsync: true, + validateOnChange: true, + }), + { + initialProps: { schema: mockSchema }, + }, + ); + + const newSchema = [{ id: 'email', validators: [], rules: [] }]; + rerender({ schema: newSchema }); + + expect(validate).toHaveBeenCalledWith(mockContext, newSchema, { abortEarly: false }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.ts new file mode 100644 index 0000000000..9b3d057558 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.ts @@ -0,0 +1,24 @@ +import { useCallback, useState } from 'react'; +import { IValidationError, IValidationSchema } from '../../../types'; +import { validate } from '../../../utils/validate'; + +export interface IUseManualValidateParams { + abortEarly?: boolean; +} + +export const useManualValidate = ( + context: object, + schema: IValidationSchema[], + params: IUseManualValidateParams = {}, +): [IValidationError[], () => void] => { + const [validationErrors, setValidationErrors] = useState([]); + + const { abortEarly = false } = params; + + const _validate = useCallback(() => { + const errors = validate(context, schema, { abortEarly }); + setValidationErrors(errors); + }, [context, schema, abortEarly]); + + return [validationErrors, _validate]; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.unit.test.ts new file mode 100644 index 0000000000..b89e53d0d8 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.unit.test.ts @@ -0,0 +1,93 @@ +import { act, renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { validate } from '../../../utils/validate'; +import { useManualValidate } from './useManualValidate'; + +vi.mock('../../../utils/validate', () => ({ + validate: vi.fn().mockReturnValue([ + { + id: 'name', + originId: 'name', + message: ['error'], + invalidValue: 'John', + }, + ]), +})); + +describe('useManualValidate', () => { + const mockContext = { name: 'John' }; + const mockSchema = [{ id: 'name', validators: [], rules: [] }]; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should initialize with empty validation errors', () => { + const { result } = renderHook(() => useManualValidate(mockContext, mockSchema)); + + expect(result.current.validationErrors).toEqual([]); + }); + + it('should validate and set errors when validate is called', () => { + const { result } = renderHook(() => useManualValidate(mockContext, mockSchema)); + + act(() => { + result.current.validate(); + }); + + expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: false }); + expect(result.current.validationErrors).toEqual([ + { + id: 'name', + originId: 'name', + message: ['error'], + invalidValue: 'John', + }, + ]); + }); + + it('should pass abortEarly param to validate function', () => { + const { result } = renderHook(() => + useManualValidate(mockContext, mockSchema, { abortEarly: true }), + ); + + act(() => { + result.current.validate(); + }); + + expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: true }); + }); + + it('should memoize validate callback with correct dependencies', () => { + const { result, rerender } = renderHook( + ({ context, schema, params }) => useManualValidate(context, schema, params), + { + initialProps: { + context: mockContext, + schema: mockSchema, + params: { abortEarly: false }, + }, + }, + ); + + const firstValidate = result.current.validate; + + // Rerender with same props + rerender({ + context: mockContext, + schema: mockSchema, + params: { abortEarly: false }, + }); + + expect(result.current.validate).toBe(firstValidate); + + // Rerender with different context + rerender({ + context: { ...mockContext, newField: 'value' } as any, + schema: mockSchema, + params: { abortEarly: false }, + }); + + expect(result.current.validate).not.toBe(firstValidate); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.ts new file mode 100644 index 0000000000..5ff722a949 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.ts @@ -0,0 +1,23 @@ +import { useMemo } from 'react'; +import { IValidationSchema } from '../../../types'; +import { validate } from '../../../utils/validate'; + +export interface IUseSyncValidateParams { + abortEarly?: boolean; + validateSync?: boolean; + validateOnChange?: boolean; +} + +export const useSyncValidate = ( + context: object, + schema: IValidationSchema[], + params: IUseSyncValidateParams = {}, +) => { + const { abortEarly = false, validateSync = false, validateOnChange = true } = params; + + return useMemo(() => { + if (!validateSync || !validateOnChange) return []; + + return validate(context, schema, { abortEarly }); + }, [context, schema, abortEarly, validateSync, validateOnChange]); +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.unit.test.ts new file mode 100644 index 0000000000..c48703a204 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.unit.test.ts @@ -0,0 +1,108 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { validate } from '../../../utils/validate'; +import { useSyncValidate } from './useSyncValidate'; + +// Mock dependencies +vi.mock('../../../utils/validate', () => ({ + validate: vi.fn().mockReturnValue([ + { + id: 'name', + originId: 'name', + message: ['error'], + invalidValue: 'John', + }, + ]), +})); + +describe('useSyncValidate', () => { + const mockContext = { name: 'John' }; + const mockSchema = [{ id: 'name', validators: [], rules: [] }]; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should initialize with empty validation errors when validateSync is false', () => { + const { result } = renderHook(() => + useSyncValidate(mockContext, mockSchema, { validateSync: false }), + ); + expect(result.current).toEqual([]); + expect(validate).not.toHaveBeenCalled(); + }); + + it('should initialize with empty validation errors when validateOnChange is false', () => { + const { result } = renderHook(() => + useSyncValidate(mockContext, mockSchema, { validateOnChange: false }), + ); + expect(result.current).toEqual([]); + expect(validate).not.toHaveBeenCalled(); + }); + + it('should validate and return errors when validateSync and validateOnChange are true', () => { + const { result } = renderHook(() => + useSyncValidate(mockContext, mockSchema, { + validateSync: true, + validateOnChange: true, + }), + ); + + expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: false }); + expect(result.current).toEqual([ + { + id: 'name', + originId: 'name', + message: ['error'], + invalidValue: 'John', + }, + ]); + }); + + it('should pass abortEarly param to validate function', () => { + renderHook(() => + useSyncValidate(mockContext, mockSchema, { + validateSync: true, + validateOnChange: true, + abortEarly: true, + }), + ); + + expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: true }); + }); + + it('should revalidate when context changes', () => { + const { rerender } = renderHook( + ({ context }) => + useSyncValidate(context, mockSchema, { + validateSync: true, + validateOnChange: true, + }), + { + initialProps: { context: mockContext }, + }, + ); + + const newContext = { name: 'Jane' }; + rerender({ context: newContext }); + + expect(validate).toHaveBeenCalledWith(newContext, mockSchema, { abortEarly: false }); + }); + + it('should revalidate when schema changes', () => { + const { rerender } = renderHook( + ({ schema }) => + useSyncValidate(mockContext, schema, { + validateSync: true, + validateOnChange: true, + }), + { + initialProps: { schema: mockSchema }, + }, + ); + + const newSchema = [{ id: 'email', validators: [], rules: [] }]; + rerender({ schema: newSchema }); + + expect(validate).toHaveBeenCalledWith(mockContext, newSchema, { abortEarly: false }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.ts new file mode 100644 index 0000000000..7c8ebaef5a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.ts @@ -0,0 +1,59 @@ +import { useMemo } from 'react'; +import { IValidationSchema } from '../../../types'; +import { useAsyncValidate } from './useAsyncValidate'; +import { useManualValidate } from './useManualValidate'; +import { useSyncValidate } from './useSyncValidate'; + +export interface IUseValidateParams { + validateOnChange?: boolean; + validateSync?: boolean; + validationDelay?: number; + abortEarly?: boolean; +} + +export const useValidate = ( + context: object, + schema: IValidationSchema[], + params: IUseValidateParams = {}, +) => { + const { + validateOnChange = true, + validateSync = false, + validationDelay = 500, + abortEarly = false, + } = params; + + const [manualValidationErrors, manualValidate] = useManualValidate(context, schema, { + abortEarly, + }); + const syncValidationErrors = useSyncValidate(context, schema, { + abortEarly, + validateOnChange, + validateSync, + }); + const asyncValidationErrors = useAsyncValidate(context, schema, { + abortEarly, + validateOnChange, + validateAsync: !validateSync, + validationDelay, + }); + + const validationErrors = useMemo(() => { + if (!validateOnChange) return manualValidationErrors; + + if (validateSync) return syncValidationErrors; + + return asyncValidationErrors; + }, [ + manualValidationErrors, + syncValidationErrors, + asyncValidationErrors, + validateOnChange, + validateSync, + ]); + + return { + errors: validationErrors, + validate: manualValidate, + }; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts new file mode 100644 index 0000000000..f1c515af7b --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts @@ -0,0 +1,525 @@ +import { renderHook } from '@testing-library/react'; +import { afterEach, describe, expect, it, test, vi } from 'vitest'; +import { IValidationError, IValidationSchema } from '../../../types'; +import * as AsyncValidateModule from './useAsyncValidate'; +import * as ManualValidateModule from './useManualValidate'; +import * as SyncValidateModule from './useSyncValidate'; +import { useValidate } from './useValidate'; + +describe('useValidate', () => { + afterEach(() => { + vi.clearAllMocks(); + vi.restoreAllMocks(); + }); + + it('should be defined', () => { + expect(useValidate).toBeDefined(); + }); + + describe('default params', () => { + it('should call manual validation hook', () => { + const context = {}; + const schema: IValidationSchema[] = []; + + const useManualValidate = vi.spyOn(ManualValidateModule, 'useManualValidate'); + + renderHook(() => useValidate(context, schema, {})); + expect(useManualValidate).toHaveBeenCalledWith(context, schema, { abortEarly: false }); + }); + + it('should call sync validation hook', () => { + const context = {}; + const schema: IValidationSchema[] = []; + + const useSyncValidate = vi.spyOn(SyncValidateModule, 'useSyncValidate'); + + renderHook(() => useValidate(context, schema, {})); + expect(useSyncValidate).toHaveBeenCalledWith(context, schema, { + abortEarly: false, + validateOnChange: true, + validateSync: false, + }); + }); + + it('should call async validation hook', () => { + const context = {}; + const schema: IValidationSchema[] = []; + + const useAsyncValidate = vi.spyOn(AsyncValidateModule, 'useAsyncValidate'); + + renderHook(() => useValidate(context, schema, {})); + expect(useAsyncValidate).toHaveBeenCalledWith(context, schema, { + abortEarly: false, + validateOnChange: true, + validateAsync: true, + validationDelay: 500, + }); + }); + }); + + describe('custom params', () => { + describe('manual validation params', () => { + const case1 = [ + { + abortEarly: true, + }, + { + abortEarly: true, + }, + ]; + + const case2 = [ + { + abortEarly: false, + }, + { + abortEarly: false, + }, + ]; + + test.each([case1, case2])( + 'should call manual validation hook with %s', + (inputParams, hookParams) => { + const context = {}; + const schema: IValidationSchema[] = []; + const useManualValidate = vi.spyOn(ManualValidateModule, 'useManualValidate'); + + renderHook(() => useValidate(context, schema, inputParams)); + expect(useManualValidate).toHaveBeenCalledWith(context, schema, hookParams); + }, + ); + }); + + describe('sync validation params', () => { + const case1 = [ + { + validateSync: true, + abortEarly: true, + validateOnChange: true, + }, + { + validateSync: true, + abortEarly: true, + validateOnChange: true, + }, + ]; + + const case2 = [ + { + validateSync: false, + abortEarly: false, + validateOnChange: false, + }, + { + validateSync: false, + abortEarly: false, + validateOnChange: false, + }, + ]; + + test.each([case1, case2])( + 'should call sync validation hook with %s', + (inputParams, hookParams) => { + const context = {}; + const schema: IValidationSchema[] = []; + const useSyncValidate = vi.spyOn(SyncValidateModule, 'useSyncValidate'); + + renderHook(() => useValidate(context, schema, inputParams)); + expect(useSyncValidate).toHaveBeenCalledWith(context, schema, hookParams); + }, + ); + }); + + describe('async validation params', () => { + const case1 = [ + { + validateSync: false, + abortEarly: true, + validateOnChange: true, + validationDelay: 500, + }, + { + validateAsync: true, + abortEarly: true, + validateOnChange: true, + validationDelay: 500, + }, + ]; + + const case2 = [ + { + validateSync: true, + abortEarly: true, + validateOnChange: true, + validationDelay: 500, + }, + { + // Validate sync disables async validation + validateAsync: false, + abortEarly: true, + validateOnChange: true, + validationDelay: 500, + }, + ]; + + test.each([case1, case2])( + 'should call async validation hook with %s', + (inputParams, hookParams) => { + const context = {}; + const schema: IValidationSchema[] = []; + const useAsyncValidate = vi.spyOn(AsyncValidateModule, 'useAsyncValidate'); + + renderHook(() => useValidate(context, schema, inputParams)); + expect(useAsyncValidate).toHaveBeenCalledWith(context, schema, hookParams); + }, + ); + }); + }); + + describe('correct validation error resolving', () => { + it('will return manual validation errors if validateOnChange is false', () => { + const context = {}; + const schema: IValidationSchema[] = [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { + type: 'required', + value: {}, + message: 'Name is required', + }, + ], + }, + ]; + const errors: IValidationError[] = [ + { + id: 'name', + originId: 'name', + invalidValue: undefined, + message: ['Name is required'], + }, + ]; + + vi.spyOn(ManualValidateModule, 'useManualValidate').mockReturnValue([errors, vi.fn()]); + + const { result } = renderHook(() => + useValidate(context, schema, { validateOnChange: false }), + ); + + expect(result.current.errors).toEqual(errors); + }); + + it('will return sync validation errors if validateOnChange is true and validateSync is true', () => { + const context = {}; + const schema: IValidationSchema[] = [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { + type: 'required', + value: {}, + message: 'Name is required', + }, + ], + }, + ]; + const errors: IValidationError[] = [ + { + id: 'name', + originId: 'name', + invalidValue: undefined, + message: ['Name is required'], + }, + ]; + + vi.spyOn(SyncValidateModule, 'useSyncValidate').mockReturnValue(errors); + + const { result } = renderHook(() => + useValidate(context, schema, { validateOnChange: true, validateSync: true }), + ); + + expect(result.current.errors).toEqual(errors); + }); + + it('will return async validation errors if validateOnChange is true and validateSync is false', () => { + const context = {}; + const schema: IValidationSchema[] = [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { + type: 'required', + value: {}, + message: 'Name is required', + }, + ], + }, + ]; + const errors: IValidationError[] = [ + { + id: 'name', + originId: 'name', + invalidValue: undefined, + message: ['Name is required'], + }, + ]; + + vi.spyOn(AsyncValidateModule, 'useAsyncValidate').mockReturnValue(errors); + + const { result } = renderHook(() => + useValidate(context, schema, { validateOnChange: true, validateSync: false }), + ); + + expect(result.current.errors).toEqual(errors); + }); + }); + + describe('validation', () => { + describe('auto validation', () => { + it('should validate context and return validation errors', async () => { + const context = {}; + const schema: IValidationSchema[] = [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { + type: 'required', + value: {}, + message: 'Name is required', + }, + ], + }, + ]; + + const { result } = renderHook(() => useValidate(context, schema)); + + expect(result.current.errors).toEqual([ + { + id: 'name', + originId: 'name', + invalidValue: undefined, + message: ['Name is required'], + }, + ]); + + vi.useRealTimers(); + }); + }); + + describe('manual validation', () => { + it('should validate context and return validation errors on validate call', async () => { + const context = {}; + const schema: IValidationSchema[] = [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { + type: 'required', + value: {}, + message: 'Name is required', + }, + ], + }, + ]; + + const { result, rerender } = renderHook(() => + useValidate(context, schema, { validateSync: true, validateOnChange: false }), + ); + + expect(result.current.errors).toEqual([]); + + result.current.validate(); + + rerender(); + + expect(result.current.errors).toEqual([ + { + id: 'name', + originId: 'name', + invalidValue: undefined, + message: ['Name is required'], + }, + ]); + }); + }); + + describe('validation on change', () => { + describe('sync', () => { + it('should validate context and return validation errors on change', () => { + let context: { name?: string } = {}; + const schema: IValidationSchema[] = [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { + type: 'required', + value: {}, + message: 'Name is required', + }, + ], + }, + ]; + const errors: IValidationError[] = [ + { + id: 'name', + originId: 'name', + invalidValue: undefined, + message: ['Name is required'], + }, + ]; + + const { result, rerender } = renderHook(() => + useValidate(context, schema, { validateOnChange: true, validateSync: true }), + ); + + expect(result.current.errors).toEqual(errors); + + context = { name: 'John' }; + + rerender(); + + expect(result.current.errors).toEqual([]); + }); + }); + + describe('async', () => { + it('should validate context and return validation errors on change', async () => { + vi.useFakeTimers(); + + let context: { name?: string } = {}; + const schema: IValidationSchema[] = [ + { + id: 'name', + valueDestination: 'name', + validators: [ + { + type: 'required', + value: {}, + message: 'Name is required', + }, + ], + }, + ]; + const errors: IValidationError[] = [ + { + id: 'name', + originId: 'name', + invalidValue: undefined, + message: ['Name is required'], + }, + ]; + + const { result, rerender } = renderHook(() => useValidate(context, schema, {})); + + expect(result.current.errors).toEqual(errors); + + context = { name: 'John' }; + + rerender(); + + await vi.runAllTimersAsync(); + + expect(result.current.errors).toEqual([]); + }); + }); + }); + }); + + describe('specific features', () => { + describe('abort early', () => { + it('should abort early if abortEarly is true and return first error', () => { + const context = {}; + const schema: IValidationSchema[] = [ + { + id: 'firstName', + valueDestination: 'firstName', + validators: [ + { + type: 'required', + value: {}, + message: 'First name is required', + }, + ], + }, + { + id: 'lastName', + valueDestination: 'lastName', + validators: [ + { + type: 'required', + value: {}, + message: 'Last name is required', + }, + ], + }, + ]; + + const { result } = renderHook(() => + useValidate(context, schema, { abortEarly: true, validateSync: true }), + ); + + expect(result.current.errors).toEqual([ + { + id: 'firstName', + originId: 'firstName', + invalidValue: undefined, + message: ['First name is required'], + }, + ]); + }); + + it('should not abort early if abortEarly is false', () => { + const context = {}; + const schema: IValidationSchema[] = [ + { + id: 'firstName', + valueDestination: 'firstName', + validators: [ + { + type: 'required', + value: {}, + message: 'First name is required', + }, + ], + }, + { + id: 'lastName', + valueDestination: 'lastName', + validators: [ + { + type: 'required', + value: {}, + message: 'Last name is required', + }, + ], + }, + ]; + + const { result } = renderHook(() => + useValidate(context, schema, { abortEarly: false, validateSync: true }), + ); + + expect(result.current.errors).toEqual([ + { + id: 'firstName', + originId: 'firstName', + invalidValue: undefined, + message: ['First name is required'], + }, + { + id: 'lastName', + originId: 'lastName', + invalidValue: undefined, + message: ['Last name is required'], + }, + ]); + }); + }); + }); +}); From c2987da62bc73b3e7978358ed0de42017e95f44b Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Wed, 4 Dec 2024 18:07:41 +0200 Subject: [PATCH 06/54] feat: finalized validator component --- .../Form/Validator/ValidatorProvider.tsx | 28 ++++++++++++++++--- .../organisms/Form/Validator/helpers.ts | 3 ++ .../Form/Validator/helpers.unit.test.ts | 17 +++++++++++ .../Form/Validator/utils/validate/validate.ts | 6 +++- 4 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/Validator/helpers.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/helpers.unit.test.ts diff --git a/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx b/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx index 4cb4397fa5..db31c294ed 100644 --- a/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx +++ b/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx @@ -1,8 +1,11 @@ -import { ValidatorContext } from './context'; +import { useMemo } from 'react'; +import { IValidatorContext, ValidatorContext } from './context'; +import { checkIfValid } from './helpers'; +import { useValidate } from './hooks/internal/useValidate'; import { IValidatorRef, useValidatorRef } from './hooks/internal/useValidatorRef'; import { IValidationSchema } from './types'; -export interface IValidatorProviderProps { +export interface IValidatorProviderProps { children: React.ReactNode | React.ReactNode[]; schema: IValidationSchema[]; value: TValue; @@ -10,15 +13,32 @@ export interface IValidatorProviderProps { ref?: React.RefObject; validateOnChange?: boolean; validateSync?: boolean; + validationDelay?: number; + abortEarly?: boolean; } -export const ValidatorProvider = ({ +export const ValidatorProvider = ({ children, schema, value, + validateOnChange, + validateSync, + abortEarly, + validationDelay, ref, }: IValidatorProviderProps) => { useValidatorRef(ref); + const { errors, validate } = useValidate(value, schema, { + abortEarly, + validateSync, + validateOnChange, + validationDelay, + }); - return {children}; + const context: IValidatorContext = useMemo( + () => ({ errors, values: value, isValid: checkIfValid(errors), validate }), + [errors, value, validate], + ); + + return {children}; }; diff --git a/packages/ui/src/components/organisms/Form/Validator/helpers.ts b/packages/ui/src/components/organisms/Form/Validator/helpers.ts new file mode 100644 index 0000000000..09a226ce1f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/helpers.ts @@ -0,0 +1,3 @@ +import { IValidationError } from './types'; + +export const checkIfValid = (errors: IValidationError[]) => errors.length === 0; diff --git a/packages/ui/src/components/organisms/Form/Validator/helpers.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/helpers.unit.test.ts new file mode 100644 index 0000000000..56e40592af --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/helpers.unit.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { checkIfValid } from './helpers'; +import { IValidationError } from './types'; + +describe('helpers', () => { + describe('checkIfValid', () => { + it('should return true if there are no errors', () => { + expect(checkIfValid([])).toBe(true); + }); + + it('should return false if there are errors', () => { + expect( + checkIfValid([{ message: 'error', element: 'element' } as unknown as IValidationError]), + ).toBe(false); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts index 77b55bbd31..f6a9aafed8 100644 --- a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts +++ b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.ts @@ -4,6 +4,7 @@ import { IValidationError, IValidationSchema, TBaseValidators, + TDeepthLevelStack, } from '../../types'; import { createValidationError } from '../create-validation-error'; import { formatValueDestination } from '../format-value-destination'; @@ -23,7 +24,10 @@ export const validate = < const validationErrors: IValidationError[] = []; - const run = (schema: Array>, stack: number[] = []) => { + const run = ( + schema: Array>, + stack: TDeepthLevelStack = [], + ) => { schema.forEach(schema => { const { validators = [], children, valueDestination, id } = schema; const formattedValueDestination = valueDestination From ce18a0c637abc3dfb58c24e78668a95a47788519 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Thu, 5 Dec 2024 16:22:07 +0200 Subject: [PATCH 07/54] feat: added story for validator --- packages/ui/package.json | 2 + .../src/components/organisms/Form/.gitignore | 1 - .../Form/Validator/Validator.stories.tsx | 10 + .../components/JsonEditor/JsonEditor.tsx | 53 +++++ .../_stories/components/JsonEditor/index.ts | 0 .../_stories/components/Story/ErrorsList.tsx | 13 ++ .../_stories/components/Story/Story.tsx | 68 +++++++ .../_stories/components/Story/context.ts | 18 ++ .../_stories/components/Story/index.ts | 1 + .../_stories/components/Story/schema.ts | 134 +++++++++++++ .../ValidatorParams/ValidatorParams.tsx | 73 +++++++ .../components/ValidatorParams/index.ts | 1 + .../organisms/Form/_Validator/Validator.tsx | 41 ---- .../_Validator/hooks/useValidate/index.ts | 3 - .../hooks/useValidate/ui-element.ts | 79 -------- .../hooks/useValidate/useValidate.ts | 24 --- ...lue-destination-and-apply-stack-indexes.ts | 10 - .../_Validator/hooks/useValidate/validate.ts | 98 --------- .../hooks/useValidatedInput/index.ts | 1 - .../useValidatedInput/useValidatedInput.ts | 8 - .../_Validator/hooks/useValidator/index.ts | 1 - .../hooks/useValidator/useValidator.ts | 4 - .../organisms/Form/_Validator/index.ts | 1 - .../organisms/Form/_Validator/types.ts | 63 ------ .../Form/_Validator/validator.context.ts | 12 -- .../_Validator/value-validator-manager.ts | 37 ---- .../document.value.validator.ts | 42 ---- .../document.value.validator.unit.test.ts | 186 ------------------ .../format.value.validator.ts | 29 --- .../format.value.validator.unit.test.ts | 58 ------ .../max-length.value.validator.ts | 26 --- .../max-length.value.validator.unit.test.ts | 63 ------ .../maximum.value.validator.ts | 23 --- .../maximum.value.validator.unit.test.ts | 63 ------ .../min-length.value.validator.ts | 26 --- .../min-length.value.validator.unit.test.ts | 63 ------ .../minimum.value.validator.ts | 23 --- .../minimum.value.validator.unit.test.ts | 63 ------ .../pattern.value.validator.ts | 23 --- .../pattern.value.validator.unit.test.ts | 54 ----- .../required.value-validator.ts | 22 --- .../required.value-validator.unit.test.ts | 57 ------ .../value-validator.abstract.ts | 9 - 43 files changed, 373 insertions(+), 1213 deletions(-) delete mode 100644 packages/ui/src/components/organisms/Form/.gitignore create mode 100644 packages/ui/src/components/organisms/Form/Validator/Validator.stories.tsx create mode 100644 packages/ui/src/components/organisms/Form/Validator/_stories/components/JsonEditor/JsonEditor.tsx create mode 100644 packages/ui/src/components/organisms/Form/Validator/_stories/components/JsonEditor/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/ErrorsList.tsx create mode 100644 packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/Story.tsx create mode 100644 packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/context.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/index.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/schema.ts create mode 100644 packages/ui/src/components/organisms/Form/Validator/_stories/components/ValidatorParams/ValidatorParams.tsx create mode 100644 packages/ui/src/components/organisms/Form/Validator/_stories/components/ValidatorParams/index.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/Validator.tsx delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/index.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/ui-element.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/useValidate.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/validate.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/index.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/useValidatedInput.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/index.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/useValidator.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/index.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/types.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/validator.context.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validator-manager.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/_Validator/value-validators/value-validator.abstract.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index 3891a490ad..fbff7a5523 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -81,6 +81,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/react": "^13.3.0", "@types/json-logic-js": "^2.0.1", + "@types/jsoneditor": "^9.9.5", "@types/lodash": "^4.14.191", "@types/node": "^20.4.1", "@types/react": "^18.0.37", @@ -95,6 +96,7 @@ "eslint-plugin-react-refresh": "^0.4.1", "eslint-plugin-storybook": "^0.6.6", "fast-glob": "^3.3.0", + "jsoneditor": "^10.1.0", "prop-types": "^15.8.1", "rimraf": "^5.0.5", "storybook": "^7.0.26", diff --git a/packages/ui/src/components/organisms/Form/.gitignore b/packages/ui/src/components/organisms/Form/.gitignore deleted file mode 100644 index 1d7655327c..0000000000 --- a/packages/ui/src/components/organisms/Form/.gitignore +++ /dev/null @@ -1 +0,0 @@ -./_Validator diff --git a/packages/ui/src/components/organisms/Form/Validator/Validator.stories.tsx b/packages/ui/src/components/organisms/Form/Validator/Validator.stories.tsx new file mode 100644 index 0000000000..79bc954a03 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/Validator.stories.tsx @@ -0,0 +1,10 @@ +import { Story } from './_stories/components/Story'; +import { ValidatorProvider } from './ValidatorProvider'; + +export default { + component: ValidatorProvider, +}; + +export const Default = { + render: () => , +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/_stories/components/JsonEditor/JsonEditor.tsx b/packages/ui/src/components/organisms/Form/Validator/_stories/components/JsonEditor/JsonEditor.tsx new file mode 100644 index 0000000000..fff6f753c3 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/_stories/components/JsonEditor/JsonEditor.tsx @@ -0,0 +1,53 @@ +import JSONEditor from 'jsoneditor'; +import 'jsoneditor/dist/jsoneditor.css'; +import { FunctionComponent, useEffect, useRef } from 'react'; + +interface IJSONEditorProps { + value: any; + readOnly?: boolean; + onChange?: (value: any) => void; +} + +export const JSONEditorComponent: FunctionComponent = ({ + value, + readOnly, + onChange, +}) => { + const containerRef = useRef(null); + const editorRef = useRef(null); + + useEffect(() => { + if (!containerRef.current) return; + + if (editorRef.current) return; + + editorRef.current = new JSONEditor(containerRef.current!, { + onChange: () => { + editorRef.current && onChange && onChange(editorRef.current.get()); + }, + }); + }, [containerRef, editorRef]); + + useEffect(() => { + if (!editorRef.current) return; + + //TODO: Each set of value rerenders editor and loses focus, find workarounds + editorRef.current.set(value); + }, [editorRef, readOnly]); + + useEffect(() => { + if (!editorRef.current) return; + + if (readOnly) { + editorRef.current.set(value); + } + }, [editorRef, readOnly, value]); + + useEffect(() => { + if (!editorRef.current) return; + + editorRef.current.setMode(readOnly ? 'view' : 'code'); + }, [readOnly]); + + return
; +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/_stories/components/JsonEditor/index.ts b/packages/ui/src/components/organisms/Form/Validator/_stories/components/JsonEditor/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/ErrorsList.tsx b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/ErrorsList.tsx new file mode 100644 index 0000000000..251df8957b --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/ErrorsList.tsx @@ -0,0 +1,13 @@ +import { useValidator } from '../../../hooks/external/useValidator'; + +export const ErrorsList = () => { + const { errors } = useValidator(); + + return ( +
+ {errors.map((error, index) => ( +
{JSON.stringify(error)}
+ ))} +
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/Story.tsx b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/Story.tsx new file mode 100644 index 0000000000..a2f2651198 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/Story.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { IValidationSchema, registerValidator, ValidatorProvider } from '../../../../Validator'; +import { JSONEditorComponent } from '../../../../Validator/_stories/components/JsonEditor/JsonEditor'; +import { ValidatorParams } from '../../../../Validator/_stories/components/ValidatorParams'; +import { initialContext } from './context'; +import { ErrorsList } from './ErrorsList'; +import { initialSchema } from './schema'; + +const evenNumbersValidator = (value: number) => { + // Ignoring validation if value is not a number + if (isNaN(value)) return true; + + if (value % 2 !== 0) { + throw new Error('Value is not even'); + } + + return true; +}; + +registerValidator('evenNumber', evenNumbersValidator); + +export const Story = () => { + const [context, setContext] = useState(initialContext); + const [validatorParams, setValidatorParams] = useState<{ + validateOnChange?: boolean; + validateSync?: boolean; + abortEarly?: boolean; + validationDelay?: number; + }>({ + validateOnChange: true, + validateSync: false, + abortEarly: false, + validationDelay: 500, + }); + const [schema, setSchema] = useState(initialSchema); + const [tempSchema, setTempSchema] = useState(initialSchema); + + return ( + +
+ setSchema(tempSchema)} + /> +
+
+

Context

+
+ +
+
+
+
+

Validation Schema

+
+
+ +
+
+
+
+ +
+
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/context.ts b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/context.ts new file mode 100644 index 0000000000..d1e6d19aa8 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/context.ts @@ -0,0 +1,18 @@ +export const initialContext = { + firstName: 'John', + lastName: 'Doe', + age: 20, + list: ['item1', 'item2', 'item3'], + nestedList: [ + { + value: 'value1', + }, + { + value: 'value2', + }, + { + value: 'value3', + sublist: [{ value: 'subitem1' }, { value: 'subitem2' }, { value: 'subitem3' }], + }, + ], +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/index.ts b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/index.ts new file mode 100644 index 0000000000..0bef493c71 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/index.ts @@ -0,0 +1 @@ +export * from './Story'; diff --git a/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/schema.ts b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/schema.ts new file mode 100644 index 0000000000..b73791c2b5 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/schema.ts @@ -0,0 +1,134 @@ +import { IValidationSchema } from '../../../types'; + +export const initialSchema: IValidationSchema[] = [ + { + id: 'firstname-field', + valueDestination: 'firstName', + validators: [ + { + type: 'required', + message: 'Name is required', + value: {}, + }, + { + type: 'minLength', + value: { minLength: 1 }, + message: 'Name must be at least {minLength} characters long', + }, + { + type: 'maxLength', + value: { maxLength: 10 }, + message: 'Name must be at most {maxLength} characters long', + }, + ], + }, + { + id: 'lastname-field', + valueDestination: 'lastName', + validators: [ + { + type: 'required', + message: 'Last name is required', + value: {}, + }, + ], + }, + { + id: 'age-field', + valueDestination: 'age', + validators: [ + { + type: 'required', + message: 'Age is required', + value: {}, + applyWhen: { + type: 'json-logic', + value: { + and: [{ '!!': { var: 'firstName' } }, { '!!': { var: 'lastName' } }], + }, + }, + }, + { + type: 'minimum', + value: { minimum: 18 }, + message: 'You must be at least {minimum} years old', + }, + { + type: 'maximum', + value: { maximum: 65 }, + message: 'You must be at most {maximum} years old', + }, + ], + }, + { + id: 'list-field', + valueDestination: 'list', + validators: [ + { + type: 'required', + message: 'List is required', + value: {}, + }, + { + type: 'minLength', + value: { minLength: 1 }, + message: 'List must be at least {minLength} items long', + }, + ], + children: [ + { + id: 'list-item', + valueDestination: 'list[$0]', + validators: [ + { + type: 'maxLength', + message: 'Item must be at most {maxLength} characters long', + value: { maxLength: 5 }, + }, + ], + }, + ], + }, + { + id: 'nested-list', + valueDestination: 'nestedList', + validators: [ + { + type: 'required', + value: {}, + message: 'Nested list is required', + }, + ], + children: [ + { + id: 'nested-list-item-value', + valueDestination: 'nestedList[$0].value', + validators: [ + { + type: 'required', + value: {}, + message: 'Nested list item value is required', + }, + ], + }, + { + id: 'nested-list-item-sublist', + valueDestination: 'nestedList[$0].sublist', + validators: [], + children: [ + { + id: 'nested-list-subitem', + valueDestination: 'nestedList[$0].sublist[$1].value', + validators: [ + { + type: 'maxLength', + value: { maxLength: 10 }, + message: 'Subitem must be at most {maxLength} characters long', + }, + ], + }, + ], + }, + ], + }, +]; diff --git a/packages/ui/src/components/organisms/Form/Validator/_stories/components/ValidatorParams/ValidatorParams.tsx b/packages/ui/src/components/organisms/Form/Validator/_stories/components/ValidatorParams/ValidatorParams.tsx new file mode 100644 index 0000000000..d20f1fc0d7 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/_stories/components/ValidatorParams/ValidatorParams.tsx @@ -0,0 +1,73 @@ +import { Button } from '@/components/atoms'; +import { Input } from '@/components/atoms/Input'; +import { Switch } from '@mui/material'; +import { useValidator } from '../../../hooks/external/useValidator'; + +interface Props { + params: { + validateOnChange?: boolean; + validateSync?: boolean; + validationDelay?: number; + abortEarly?: boolean; + }; + onChange: (params: Props['params']) => void; + onSave: () => void; +} + +export const ValidatorParams = ({ params, onChange, onSave }: Props) => { + const { validate } = useValidator(); + + return ( +
+ + + + + {!params.validateOnChange && ( + + )} + +
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/Validator/_stories/components/ValidatorParams/index.ts b/packages/ui/src/components/organisms/Form/Validator/_stories/components/ValidatorParams/index.ts new file mode 100644 index 0000000000..84d65a1d63 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/Validator/_stories/components/ValidatorParams/index.ts @@ -0,0 +1 @@ +export * from './ValidatorParams'; diff --git a/packages/ui/src/components/organisms/Form/_Validator/Validator.tsx b/packages/ui/src/components/organisms/Form/_Validator/Validator.tsx deleted file mode 100644 index 16b073fc29..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/Validator.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useValidate } from '@/components/providers/Validator/hooks/useValidate'; -import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; -import { UIElementV2 } from '@/components/providers/Validator/types'; -import React, { FunctionComponent, useCallback, useMemo, useState } from 'react'; -import { TValidationErrors, validatorContext } from './validator.context'; - -const { Provider } = validatorContext; - -export interface IValidatorProps { - children: React.ReactNode | React.ReactNode[]; - context: unknown; - elements: UIElementV2[]; -} - -export const Validator: FunctionComponent = ({ children, elements, context }) => { - const validate = useValidate({ elements, context }); - const [validationErrors, setValiationErrors] = useState({}); - - console.log({ validationErrors }); - - const onValidate = useCallback(() => { - const errors = validate(); - const validationErrors = errors.reduce((acc, error) => { - const element = new UIElement(error.element, context, error.stack); - acc[element.getId()] = [...(acc[element.getId()] || []), error.message]; - - return acc; - }, {} as TValidationErrors); - - setValiationErrors(validationErrors); - - return Boolean(errors.length); - }, [validate, context]); - - const ctx = useMemo( - () => ({ validate: onValidate, errors: validationErrors }), - [validationErrors, onValidate], - ); - - return {children}; -}; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/index.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/index.ts deleted file mode 100644 index 2ae47e498d..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './useValidate'; -export * from './utils/format-value-destination-and-apply-stack-indexes'; -export * from './validate'; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/ui-element.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/ui-element.ts deleted file mode 100644 index 4e42f0544d..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/ui-element.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { testRule } from '@/components/organisms/DynamicUI/rule-engines/utils/execute-rules'; -import { formatValueDestinationAndApplyStackIndexes } from '@/components/providers/Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes'; -import { TValidationParams, UIElementV2 } from '@/components/providers/Validator/types'; -import { IDocumentValueValidatorParams } from '@/components/providers/Validator/value-validators/document.value.validator'; -import { IRequiredValueValidatorParams } from '@/components/providers/Validator/value-validators/required.value-validator'; -import { fieldElelements } from '@/pages/CollectionFlowV2/renderer-schema'; -import { AnyObject } from '@ballerine/ui'; -import get from 'lodash/get'; - -export class UIElement { - constructor(readonly element: UIElementV2, readonly context: unknown, readonly stack: number[]) {} - - getId() { - return this.formatId(this.element.id); - } - - getOriginId() { - return this.element.id; - } - - isRequired() { - const requiredParams = this.element.validation?.required as IRequiredValueValidatorParams; - const documentParams = this.element.validation?.document as IDocumentValueValidatorParams; - - const applyRules = requiredParams?.applyWhen || documentParams?.applyWhen || []; - - if (applyRules.length) { - const isShouldApplyRequired = applyRules.every(rule => - testRule(this.context as AnyObject, rule), - ); - - return Boolean(isShouldApplyRequired); - } else { - return Boolean(requiredParams?.required) || Boolean(documentParams?.documentId); - } - } - - private formatId(id: string) { - return `${id}${this.stack.join('.')}`; - } - - getValueDestination() { - return this.formatValueDestination(this.element.valueDestination); - } - - private formatValueDestination(valueDestination: string) { - return this.formatValueDestinationAndApplyStackIndexes(valueDestination); - } - - private formatValueDestinationAndApplyStackIndexes(valueDestination: string) { - return formatValueDestinationAndApplyStackIndexes(valueDestination, this.stack); - } - - getValue() { - const valueDestination = this.getValueDestination(); - - return get(this.context, valueDestination); - } - - getValidatorsParams() { - const validatorKeys = Object.keys(this.element.validation || {}); - - return validatorKeys.map(key => ({ - validator: key, - params: this.element.validation[key as keyof UIElementV2['validation']] as TValidationParams, - })); - } - - getDefinition() { - return this.element; - } - - getFieldType() { - if (this.element.element === 'fieldlist') return 'field-list'; - if (this.element.element in fieldElelements) return 'field'; - - return 'ui'; - } -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/useValidate.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/useValidate.ts deleted file mode 100644 index a36c7d1eea..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/useValidate.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { UIElementV2 } from '@/components/providers/Validator/types'; -import { useCallback } from 'react'; -import { validate as _validate } from './validate'; - -interface IUseValidateParams { - elements: UIElementV2[]; - context: unknown; -} - -export interface IValidationError { - id: string; - message: string; - element: UIElementV2; - valueDestination: string; - stack: number[]; -} - -export const useValidate = ({ elements, context }: IUseValidateParams) => { - const validate = useCallback(() => { - return _validate(elements, context as object); - }, [elements, context]); - - return validate; -}; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts deleted file mode 100644 index 3c5b32e485..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const formatValueDestinationAndApplyStackIndexes = ( - valueDestination: string, - stack: number[], -) => { - stack.forEach((stackValue, index) => { - valueDestination = valueDestination.replace(`$${index}`, String(stackValue)); - }); - - return valueDestination; -}; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/validate.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/validate.ts deleted file mode 100644 index bba92367a0..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidate/validate.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { testRule } from '@/components/organisms/DynamicUI/rule-engines/utils/execute-rules'; -import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; -import { IValidationError } from '@/components/providers/Validator/hooks/useValidate/useValidate'; -import { IBaseValueValidatorParams, UIElementV2 } from '@/components/providers/Validator/types'; -import { ValueValidatorManager } from '@/components/providers/Validator/value-validator-manager'; -import { AnyObject } from '@ballerine/ui'; - -export const validate = (elements: UIElementV2[], context: object) => { - const validatorManager = new ValueValidatorManager(); - let errors: IValidationError[] = []; - - const fieldValidationStrategy = ( - element: UIElement, - stack: number[] = [], - ): IValidationError[] => { - const value = element.getValue(); - const fieldContext = { - context, - stack, - }; - - const isShouldApplyValidation = ( - params: TParams, - context: AnyObject, - ) => { - const applyRules = params.applyWhen && params.applyWhen ? params.applyWhen : null; - - if (!applyRules) return true; - - return applyRules.length && applyRules.every(rule => testRule(context, rule)); - }; - - const validationErrors = element.getValidatorsParams().map(({ validator, params }) => { - try { - if (validator === 'required' || validator === 'document') { - const isRequired = element.isRequired(); - if (!isRequired && value === undefined) return; - - validatorManager.validate(value, validator as any, params, fieldContext); - } else { - if (!isShouldApplyValidation(params as unknown as IBaseValueValidatorParams, context)) - return; - - if (value === undefined) return; - - validatorManager.validate(value, validator as any, params, fieldContext); - } - } catch (error) { - return { - //@ts-ignore - message: error.message, - element: element.element, - id: element.getId(), - valueDestination: element.getValueDestination(), - stack, - }; - } - }); - - return validationErrors.filter(Boolean) as IValidationError[]; - }; - - const validateFn = (elements: UIElementV2[], context: object, stack: number[] = []) => { - for (let i = 0; i < elements.length; i++) { - const element = elements[i] as UIElementV2; - const uiElement = new UIElement(element, context, stack); - if (!element) continue; - - if (uiElement.getFieldType() === 'field') { - errors = [...errors, ...fieldValidationStrategy(uiElement, stack)]; - - continue; - } - - if (uiElement.getFieldType() === 'field-list') { - const value = uiElement.getValue() as any[]; - - errors = [...errors, ...fieldValidationStrategy(uiElement, stack)]; - - if (!Array.isArray(value)) continue; - - value?.forEach((_, index) => { - validateFn(element.children!, context, [...stack, index]); - }); - - continue; - } - - if (element.children) { - validateFn(element.children, context, stack); - } - } - }; - - validateFn(elements, context as any); - - return errors; -}; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/index.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/index.ts deleted file mode 100644 index b6fb03d1bc..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useValidatedInput'; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/useValidatedInput.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/useValidatedInput.ts deleted file mode 100644 index 7daa7ec242..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidatedInput/useValidatedInput.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; -import { useValidator } from '@/components/providers/Validator/hooks/useValidator'; - -export const useValidatedInput = (element: UIElement) => { - const { errors } = useValidator(); - - return errors[element.getId()]; -}; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/index.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/index.ts deleted file mode 100644 index df0ef89dfd..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useValidator'; diff --git a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/useValidator.ts b/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/useValidator.ts deleted file mode 100644 index f4889599d5..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/hooks/useValidator/useValidator.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { validatorContext } from '@/components/providers/Validator/validator.context'; -import { useContext } from 'react'; - -export const useValidator = () => useContext(validatorContext); diff --git a/packages/ui/src/components/organisms/Form/_Validator/index.ts b/packages/ui/src/components/organisms/Form/_Validator/index.ts deleted file mode 100644 index 3ed72221f5..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Validator'; diff --git a/packages/ui/src/components/organisms/Form/_Validator/types.ts b/packages/ui/src/components/organisms/Form/_Validator/types.ts deleted file mode 100644 index 77580a1dfb..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/types.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { IDocumentValueValidatorParams } from '@/components/providers/Validator/value-validators/document.value.validator'; -import { IFormatValueValidatorParams } from '@/components/providers/Validator/value-validators/format.value.validator'; -import { IMaxLengthValueValidatorParams } from '@/components/providers/Validator/value-validators/max-length.value.validator'; -import { IMaximumValueValidatorParams } from '@/components/providers/Validator/value-validators/maximum.value.validator'; -import { IMinLengthValueValidatorParams } from '@/components/providers/Validator/value-validators/min-length.value.validator'; -import { IPatternValidatorParams } from '@/components/providers/Validator/value-validators/pattern.value.validator'; -import { IRequiredValueValidatorParams } from '@/components/providers/Validator/value-validators/required.value-validator'; -import { Rule } from '@/domains/collection-flow'; -import { AnyObject } from '@ballerine/ui'; - -export type TFormats = 'email'; - -export type TValidatorErrorMessage = string; - -export type TValidatorApplyRule = object; - -export type TValidationParams = - | IFormatValueValidatorParams - | IMaxLengthValueValidatorParams - | IMinLengthValueValidatorParams - | IMaximumValueValidatorParams - | IMinLengthValueValidatorParams - | IRequiredValueValidatorParams - | IPatternValidatorParams - | IDocumentValueValidatorParams; - -export type TValidators = - | 'required' - | 'minLength' - | 'maxLength' - | 'pattern' - | 'minimum' - | 'maximum' - | 'format' - | 'document'; - -export interface IBaseFieldParams { - label?: string; - placeholder?: string; - stack?: number[]; -} - -export interface UIElementV2 { - id: string; - element: string; - validation: Partial>; - options?: TFieldParams; - valueDestination: string; - children?: UIElementV2[]; - - availableOn?: Rule[]; - visibleOn?: Rule[]; -} - -export interface IBaseValueValidatorParams { - message?: string; - applyWhen?: Rule[]; -} - -export interface IFieldContext { - context: AnyObject; - stack: number[]; -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/validator.context.ts b/packages/ui/src/components/organisms/Form/_Validator/validator.context.ts deleted file mode 100644 index 427b6885d6..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/validator.context.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createContext } from 'react'; - -type TIsValid = boolean; -type TFielName = string; - -export type TValidationErrors = Record; -export interface IValidatorContext { - validate: () => TIsValid; - errors: TValidationErrors; -} - -export const validatorContext = createContext({} as IValidatorContext); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validator-manager.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validator-manager.ts deleted file mode 100644 index 01aea4fa49..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validator-manager.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { IBaseValueValidatorParams, IFieldContext } from '@/components/providers/Validator/types'; -import { DocumentValueValidator } from '@/components/providers/Validator/value-validators/document.value.validator'; -import { FormatValueValidator } from '@/components/providers/Validator/value-validators/format.value.validator'; -import { MaxLengthValueValidator } from '@/components/providers/Validator/value-validators/max-length.value.validator'; -import { MaximumValueValidator } from '@/components/providers/Validator/value-validators/maximum.value.validator'; -import { MinLengthValueValidator } from '@/components/providers/Validator/value-validators/min-length.value.validator'; -import { MinimumValueValidator } from '@/components/providers/Validator/value-validators/minimum.value.validator'; -import { PatternValueValidator } from '@/components/providers/Validator/value-validators/pattern.value.validator'; -import { RequiredValueValidator } from '@/components/providers/Validator/value-validators/required.value-validator'; - -const validatorsMap = { - required: RequiredValueValidator, - minLength: MinLengthValueValidator, - maxLength: MaxLengthValueValidator, - pattern: PatternValueValidator, - minimum: MinimumValueValidator, - maximum: MaximumValueValidator, - format: FormatValueValidator, - document: DocumentValueValidator, -}; - -export type TValidator = keyof typeof validatorsMap; - -export class ValueValidatorManager { - constructor(readonly validators: typeof validatorsMap = validatorsMap) {} - - validate( - value: unknown, - key: TValidator, - params: TValidatorParams, - fieldContext: IFieldContext, - ) { - const validator = new this.validators[key](params as any); - //@ts-ignore - return validator.validate(value, fieldContext); - } -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.ts deleted file mode 100644 index 6473cd72a0..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { formatValueDestinationAndApplyStackIndexes } from '@/components/providers/Validator/hooks/useValidate'; -import { IBaseValueValidatorParams, IFieldContext } from '@/components/providers/Validator/types'; -import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; -import { Document } from '@/domains/collection-flow'; -import get from 'lodash/get'; - -export interface IDocumentValueValidatorParams extends IBaseValueValidatorParams { - documentId: string; - pathToDocuments: string; - - // Page index to check file id from, defaults to 0 - pageIndex?: number; -} - -export class DocumentValueValidator extends ValueValidator { - type = 'document'; - - validate(_: unknown, fieldContext: IFieldContext): void { - const { pathToDocuments, documentId, pageIndex = 0 } = this.params; - - const documentsPath = this.getDocumentsPathWithIndexes(pathToDocuments, fieldContext); - const documents = get(fieldContext.context, documentsPath); - - const document = documents?.find((document: Document) => document.id === documentId); - - debugger; - - if (!document || !document.pages?.[pageIndex]?.ballerineFileId) { - throw new Error(this.getErrorMessage()); - } - } - - private getErrorMessage() { - if (!this.params.message) return `Document is required.`; - - return this.params.message; - } - - private getDocumentsPathWithIndexes(documentsPath: string, fieldContext: IFieldContext) { - return formatValueDestinationAndApplyStackIndexes(documentsPath, fieldContext.stack || []); - } -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.unit.test.ts deleted file mode 100644 index c932511519..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/document.value.validator.unit.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { DocumentValueValidator } from './document.value.validator'; - -describe('DocumentValueValidator', () => { - describe('validation will fail', () => { - test('when document is not found', () => { - const validator = new DocumentValueValidator({ - documentId: '123', - pathToDocuments: 'form.documents', - }); - - expect(() => validator.validate(null as any, {} as any)).toThrowError( - 'Document is required.', - ); - }); - - test('when document not found at specific page index', () => { - const validator = new DocumentValueValidator({ - documentId: '123', - pathToDocuments: 'form.documents', - pageIndex: 1, - }); - - expect(() => validator.validate(null as any, {} as any)).toThrowError( - 'Document is required.', - ); - }); - - test('when document is not found in nested data structure', () => { - const validator = new DocumentValueValidator({ - documentId: '123', - pathToDocuments: 'data.items[$0].documents', - }); - - expect(() => - validator.validate( - null as any, - { - stack: [1], - context: { - data: { - items: [ - { - documents: [], - }, - { - documents: [], - }, - ], - }, - }, - } as any, - ), - ).toThrowError('Document is required.'); - }); - - test('when document not found at specific page index in nested data structure', () => { - const validator = new DocumentValueValidator({ - documentId: '123', - pathToDocuments: 'data.items[$0].documents', - pageIndex: 1, - }); - - expect(() => - validator.validate( - null as any, - { - stack: [1], - context: {}, - } as any, - ), - ).toThrowError('Document is required.'); - }); - }); - - describe('validation will pass', () => { - test('when document is found', () => { - const validator = new DocumentValueValidator({ - documentId: '123', - pathToDocuments: 'documents', - }); - - const context = { - documents: [ - { - id: '123', - pages: [ - { - ballerineFileId: 'someFileId', - }, - ], - }, - ], - }; - - expect(() => validator.validate(null as any, { context, stack: [] })).not.toThrow(); - }); - - test('when document is found at specific page index', () => { - const validator = new DocumentValueValidator({ - documentId: '123', - pathToDocuments: 'documents', - pageIndex: 1, - }); - - const context = { - documents: [ - { - id: '123', - pages: [ - {}, - { - ballerineFileId: 'someFileId', - }, - ], - }, - ], - }; - - expect(() => validator.validate(null as any, { context, stack: [] })).not.toThrow(); - }); - - test.each([ - ['single level nesting', 'data.items[$0].documents', [0]], - ['two levels of nesting', 'data.items[$0].subitems[$1].documents', [0, 1]], - [ - 'three levels of nesting', - 'data.items[$0].subitems[$1].subsubitems[$2].documents', - [0, 1, 1], - ], - ])('when document is found in nested data structure - %s', (_, pathToDocuments, stack) => { - const validator = new DocumentValueValidator({ - documentId: '123', - pathToDocuments, - }); - - const context = { - data: { - items: [ - { - documents: [{ id: '123', pages: [{ ballerineFileId: 'someFileId' }] }], - subitems: [ - {}, - { - documents: [{ id: '123', pages: [{ ballerineFileId: 'someFileId' }] }], - subsubitems: [ - {}, - { - documents: [{ id: '123', pages: [{ ballerineFileId: 'someFileId' }] }], - }, - ], - }, - ], - }, - ], - }, - }; - - expect(() => validator.validate(null as any, { context, stack })).not.toThrow(); - }); - }); - - describe('validator error messages', () => { - test('should return default error message when message is not provided', () => { - const validator = new DocumentValueValidator({ - documentId: '123', - pathToDocuments: 'documents', - }); - - expect(() => validator.validate(null as any, { context: {}, stack: [] })).toThrowError( - 'Document is required.', - ); - }); - - test('should return custom error message when message is provided', () => { - const validator = new DocumentValueValidator({ - documentId: '123', - pathToDocuments: 'documents', - message: 'Custom error message.', - }); - - expect(() => validator.validate(null as any, { context: {}, stack: [] })).toThrowError( - 'Custom error message.', - ); - }); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.ts deleted file mode 100644 index 16efc39129..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IBaseValueValidatorParams, TFormats } from '@/components/providers/Validator/types'; -import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; -import EmailValidator from 'email-validator'; - -export interface IFormatValueValidatorParams extends IBaseValueValidatorParams { - format: TFormats; -} - -export class FormatValueValidator extends ValueValidator { - type = 'format'; - - validate(value: unknown): void { - if (this.params.format === 'email') { - if (!EmailValidator.validate(value as string)) { - throw new Error(this.getErrorMessage()); - } - - return; - } - - throw new Error(`Format ${this.params.format} is not supported.`); - } - - private getErrorMessage() { - if (!this.params.message) return 'Invalid format.'; - - return this.params.message.replace('{format}', this.params.format.toString()); - } -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.unit.test.ts deleted file mode 100644 index 47c440d222..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/format.value.validator.unit.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { FormatValueValidator } from '@/components/providers/Validator/value-validators/format.value.validator'; - -describe('Format Value Validator', () => { - describe('validation will fail', () => { - test('when value does not match format', () => { - const validator = new FormatValueValidator({ format: 'email', message: 'error' }); - - expect(() => validator.validate('abc')).toThrowError('error'); - }); - - test('when value is null', () => { - const validator = new FormatValueValidator({ format: 'email', message: 'error' }); - - expect(() => validator.validate(null as any)).toThrowError('error'); - }); - - test('when unsupported format is provided', () => { - //@ts-ignore - const validator = new FormatValueValidator({ format: 'unsupported', message: 'error' }); - - expect(() => validator.validate('abc')).toThrowError('Format unsupported is not supported.'); - }); - }); - - describe('validation will pass', () => { - test('when value matches format', () => { - const validator = new FormatValueValidator({ format: 'email', message: 'error' }); - - expect(() => validator.validate('example@gmail.com')).not.toThrow(); - }); - }); - - describe('validator error messages', () => { - test('should return default error message when message is not provided', () => { - const validator = new FormatValueValidator({ format: 'email' }); - - expect(() => validator.validate('abc')).toThrowError('Invalid format.'); - }); - - test('should return custom error message when message is provided', () => { - const validator = new FormatValueValidator({ format: 'email', message: 'error' }); - - expect(() => validator.validate('abc')).toThrowError('error'); - }); - - test('should interpolate {format} with the provided value', () => { - const validator = new FormatValueValidator({ format: 'email', message: 'error {format}' }); - - expect(() => validator.validate('abc')).toThrowError('error email'); - }); - - test('error message should stay same if interlopation tag is not present', () => { - const validator = new FormatValueValidator({ format: 'email', message: 'error' }); - - expect(() => validator.validate('abc')).toThrowError('error'); - }); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.ts deleted file mode 100644 index 2bc9f8d018..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; -import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; - -export interface IMaxLengthValueValidatorParams extends IBaseValueValidatorParams { - maxLength: number; -} - -export class MaxLengthValueValidator extends ValueValidator { - type = 'maxLength'; - - validate(value: TValue): void { - if (value?.length === undefined || value.length > this.params.maxLength) { - throw new Error(this.getErrorMessage()); - } - } - - private getErrorMessage() { - if (!this.params.message) - return `Maximum length is {maxLength}.`.replace( - '{maxLength}', - this.params.maxLength.toString(), - ); - - return this.params.message.replace('{maxLength}', this.params.maxLength.toString()); - } -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.unit.test.ts deleted file mode 100644 index 5f8b758cbb..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/max-length.value.validator.unit.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MaxLengthValueValidator } from '@/components/providers/Validator/value-validators/max-length.value.validator'; - -describe('Max Length Value Validator', () => { - describe('validation will fail', () => { - test('when value is undefined', () => { - const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); - - expect(() => validator.validate(undefined as any)).toThrowError('error'); - }); - - test('when value is above maximum length', () => { - const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); - - expect(() => validator.validate('123456')).toThrowError('error'); - }); - - test('when value is null', () => { - const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); - - expect(() => validator.validate(null as any)).toThrowError('error'); - }); - }); - - describe('validation will pass', () => { - test('when value is below maximum length', () => { - const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); - - expect(() => validator.validate('12345')).not.toThrow(); - }); - - test('when value is equal to maximum length', () => { - const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); - - expect(() => validator.validate('12345')).not.toThrow(); - }); - }); - - describe('validator error messages', () => { - test('should return default error message when message is not provided', () => { - const validator = new MaxLengthValueValidator({ maxLength: 5 }); - - expect(() => validator.validate('123456')).toThrowError('Maximum length is 5.'); - }); - - test('should return custom error message when message is provided', () => { - const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); - - expect(() => validator.validate('123456')).toThrowError('error'); - }); - - test('should interpolate {maxLength} with the provided value', () => { - const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error {maxLength}' }); - - expect(() => validator.validate('123456')).toThrowError('error 5'); - }); - - test('error message should stay same if interlopation tag is not present', () => { - const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); - - expect(() => validator.validate('123456')).toThrowError('error'); - }); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.ts deleted file mode 100644 index 55bf6ae8ce..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; -import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; - -export interface IMaximumValueValidatorParams extends IBaseValueValidatorParams { - maximum: number; -} - -export class MaximumValueValidator extends ValueValidator { - type = 'maximum'; - - validate(value: TValue): void { - if (typeof value !== 'number' || value > this.params.maximum) { - throw new Error(this.getErrorMessage()); - } - } - - private getErrorMessage() { - if (!this.params.message) - return `Maximum value is {maximum}.`.replace('{maximum}', this.params.maximum.toString()); - - return this.params.message.replace('{maximum}', this.params.maximum.toString()); - } -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.unit.test.ts deleted file mode 100644 index 4cceba2b16..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/maximum.value.validator.unit.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MaximumValueValidator } from '@/components/providers/Validator/value-validators/maximum.value.validator'; - -describe('Maximum Value Validator', () => { - describe('validation will fail', () => { - test('when value is undefined', () => { - const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); - - expect(() => validator.validate(undefined as any)).toThrowError('error'); - }); - - test('when value is above maximum', () => { - const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); - - expect(() => validator.validate(6)).toThrowError('error'); - }); - - test('when value is null', () => { - const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); - - expect(() => validator.validate(null as any)).toThrowError('error'); - }); - }); - - describe('validation will pass', () => { - test('when value is below maximum', () => { - const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); - - expect(() => validator.validate(4)).not.toThrow(); - }); - - test('when value is equal to maximum', () => { - const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); - - expect(() => validator.validate(5)).not.toThrow(); - }); - }); - - describe('validator error messages', () => { - test('should return default error message when message is not provided', () => { - const validator = new MaximumValueValidator({ maximum: 5 }); - - expect(() => validator.validate(6)).toThrowError('Maximum value is 5.'); - }); - - test('should return custom error message when message is provided', () => { - const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); - - expect(() => validator.validate(6)).toThrowError('error'); - }); - - test('should interpolate {maximum} with the provided value', () => { - const validator = new MaximumValueValidator({ maximum: 5, message: 'error {maximum}' }); - - expect(() => validator.validate(6)).toThrowError('error 5'); - }); - - test('error message should stay same if interlopation tag is not present', () => { - const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); - - expect(() => validator.validate(6)).toThrowError('error'); - }); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.ts deleted file mode 100644 index 04eb9ff5b3..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; -import { IBaseValueValidatorParams } from '../types'; - -export interface IMinLengthValueValidatorParams extends IBaseValueValidatorParams { - minLength: number; -} - -export class MinLengthValueValidator extends ValueValidator { - type = 'minLength'; - - validate(value: TValue): void { - if (value?.length === undefined || value.length < this.params.minLength) { - throw new Error(this.getErrorMessage()); - } - } - - private getErrorMessage() { - if (!this.params.message) - return `Minimum length is {minLength}.`.replace( - '{minLength}', - this.params.minLength.toString(), - ); - - return this.params.message.replace('{minLength}', this.params.minLength.toString()); - } -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.unit.test.ts deleted file mode 100644 index 68e7f607de..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/min-length.value.validator.unit.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MinLengthValueValidator } from '@/components/providers/Validator/value-validators/min-length.value.validator'; - -describe('MinLength Value Validator', () => { - describe('validation will fail', () => { - test('when value is undefined', () => { - const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); - - expect(() => validator.validate(undefined as any)).toThrowError('error'); - }); - - test('when value is below minimum length', () => { - const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); - - expect(() => validator.validate('1234')).toThrowError('error'); - }); - - test('when value is null', () => { - const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); - - expect(() => validator.validate(null as any)).toThrowError('error'); - }); - }); - - describe('validation will pass', () => { - test('when value is above minimum length', () => { - const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); - - expect(() => validator.validate('12345')).not.toThrow(); - }); - - test('when value is equal to minimum length', () => { - const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); - - expect(() => validator.validate('12345')).not.toThrow(); - }); - }); - - describe('validator error messages', () => { - test('should return default error message when message is not provided', () => { - const validator = new MinLengthValueValidator({ minLength: 5 }); - - expect(() => validator.validate('1234')).toThrowError('Minimum length is 5.'); - }); - - test('should return custom error message when message is provided', () => { - const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); - - expect(() => validator.validate('1234')).toThrowError('error'); - }); - - test('should interpolate {minLength} with the provided value', () => { - const validator = new MinLengthValueValidator({ minLength: 5, message: 'error {minLength}' }); - - expect(() => validator.validate('1234')).toThrowError('error 5'); - }); - - test('error message should stay same if interlopation tag is not present', () => { - const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); - - expect(() => validator.validate('1234')).toThrowError('error'); - }); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.ts deleted file mode 100644 index 1e08f15ba4..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; -import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; - -export interface IMinimumValueValidatorParams extends IBaseValueValidatorParams { - minimum: number; -} - -export class MinimumValueValidator extends ValueValidator { - type = 'minimum'; - - validate(value: TValue): void { - if (typeof value !== 'number' || value < this.params.minimum) { - throw new Error(this.getErrorMessage()); - } - } - - private getErrorMessage() { - if (!this.params.message) - return `Minimum value is {minimum}.`.replace('{minimum}', this.params.minimum.toString()); - - return this.params.message.replace('{minimum}', this.params.minimum.toString()); - } -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.unit.test.ts deleted file mode 100644 index 61bc62bd29..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/minimum.value.validator.unit.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MinimumValueValidator } from '@/components/providers/Validator/value-validators/minimum.value.validator'; - -describe('MinimumValueValidator', () => { - describe('validation will fail', () => { - test('when value is undefined', () => { - const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); - - expect(() => validator.validate(undefined as any)).toThrowError('error'); - }); - - test('when value is below minimum', () => { - const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); - - expect(() => validator.validate(4)).toThrowError('error'); - }); - - test('when value is null', () => { - const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); - - expect(() => validator.validate(null as any)).toThrowError('error'); - }); - }); - - describe('validation will pass', () => { - test('when value is above minimum', () => { - const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); - - expect(() => validator.validate(5)).not.toThrow(); - }); - - test('when value is equal to minimum', () => { - const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); - - expect(() => validator.validate(5)).not.toThrow(); - }); - }); - - describe('validator error messages', () => { - test('should return default error message when message is not provided', () => { - const validator = new MinimumValueValidator({ minimum: 5 }); - - expect(() => validator.validate(4)).toThrowError('Minimum value is 5.'); - }); - - test('should return custom error message when message is provided', () => { - const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); - - expect(() => validator.validate(4)).toThrowError('error'); - }); - - test('should interpolate {minimum} with the provided value', () => { - const validator = new MinimumValueValidator({ minimum: 5, message: 'error {minimum}' }); - - expect(() => validator.validate(4)).toThrowError('error 5'); - }); - - test('error message should stay same if interlopation tag is not present', () => { - const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); - - expect(() => validator.validate(4)).toThrowError('error'); - }); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.ts deleted file mode 100644 index b2cd0302e1..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; -import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; - -export interface IPatternValidatorParams extends IBaseValueValidatorParams { - pattern: string; -} - -export class PatternValueValidator extends ValueValidator { - type = 'pattern'; - - validate(value: unknown) { - if (!new RegExp(this.params.pattern).test(value as string)) { - throw new Error(this.getErrorMessage()); - } - } - - private getErrorMessage() { - if (!this.params.message) - return `Value must match {pattern}.`.replace('{pattern}', this.params.pattern); - - return this.params.message.replace('{pattern}', this.params.pattern); - } -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.unit.test.ts deleted file mode 100644 index b305bcb250..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/pattern.value.validator.unit.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { PatternValueValidator } from '@/components/providers/Validator/value-validators/pattern.value.validator'; - -describe('Pattern Value Validator', () => { - describe('validation will fail', () => { - test('when value does not match pattern', () => { - const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); - - expect(() => validator.validate('abc')).toThrowError('error'); - }); - - test('when value is null', () => { - const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); - - expect(() => validator.validate(null as any)).toThrowError('error'); - }); - }); - - describe('validation will pass', () => { - test('when value matches pattern', () => { - const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); - - expect(() => validator.validate('123')).not.toThrow(); - }); - }); - - describe('validator error messages', () => { - test('should return default error message when message is not provided', () => { - const validator = new PatternValueValidator({ pattern: '^[0-9]+$' }); - - expect(() => validator.validate('abc')).toThrowError('Value must match ^[0-9]+$.'); - }); - - test('should return custom error message when message is provided', () => { - const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); - - expect(() => validator.validate('abc')).toThrowError('error'); - }); - - test('should interpolate {pattern} with the provided value', () => { - const validator = new PatternValueValidator({ - pattern: '^[0-9]+$', - message: 'error {pattern}', - }); - - expect(() => validator.validate('abc')).toThrowError('error ^[0-9]+$'); - }); - - test('error message should stay same if interlopation tag is not present', () => { - const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); - - expect(() => validator.validate('abc')).toThrowError('error'); - }); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.ts deleted file mode 100644 index bdaaeb20f6..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; -import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; - -export interface IRequiredValueValidatorParams extends IBaseValueValidatorParams { - required: boolean; -} - -export class RequiredValueValidator extends ValueValidator { - type = 'required'; - - validate(value: unknown) { - if (value === undefined || value === null || value === '') { - throw new Error(this.getErrorMessage()); - } - } - - private getErrorMessage() { - if (!this.params.message) return `Value is required.`; - - return this.params.message; - } -} diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.unit.test.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.unit.test.ts deleted file mode 100644 index a752045689..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/required.value-validator.unit.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { RequiredValueValidator } from '@/components/providers/Validator/value-validators/required.value-validator'; - -describe('Required Value Validator', () => { - describe('validation will fail', () => { - test('when value is undefined', () => { - const validator = new RequiredValueValidator({ message: 'error', required: true }); - - expect(() => validator.validate(undefined as any)).toThrowError('error'); - }); - - test('when value is empty string', () => { - const validator = new RequiredValueValidator({ message: 'error', required: true }); - - expect(() => validator.validate('')).toThrowError('error'); - }); - - test('when value is null', () => { - const validator = new RequiredValueValidator({ message: 'error', required: true }); - - expect(() => validator.validate(null as any)).toThrowError('error'); - }); - }); - - describe('validation will pass', () => { - test('when value is not empty string', () => { - const validator = new RequiredValueValidator({ message: 'error', required: true }); - - expect(() => validator.validate('value')).not.toThrow(); - }); - - test('when value is not null', () => { - const validator = new RequiredValueValidator({ message: 'error', required: true }); - - expect(() => validator.validate(0)).not.toThrow(); - }); - - test('when value is not undefined', () => { - const validator = new RequiredValueValidator({ message: 'error', required: true }); - - expect(() => validator.validate(false)).not.toThrow(); - }); - }); - - describe('validator error messages', () => { - test('should return default error message when message is not provided', () => { - const validator = new RequiredValueValidator({ required: true }); - - expect(() => validator.validate('')).toThrowError('Value is required.'); - }); - - test('should return custom error message when message is provided', () => { - const validator = new RequiredValueValidator({ message: 'error', required: true }); - - expect(() => validator.validate('')).toThrowError('error'); - }); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/_Validator/value-validators/value-validator.abstract.ts b/packages/ui/src/components/organisms/Form/_Validator/value-validators/value-validator.abstract.ts deleted file mode 100644 index d45f4d9b13..0000000000 --- a/packages/ui/src/components/organisms/Form/_Validator/value-validators/value-validator.abstract.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IFieldContext } from '@/components/providers/Validator/types'; - -export abstract class ValueValidator { - abstract type: string; - - constructor(readonly params: TParams) {} - - abstract validate(value: unknown, fieldContext: IFieldContext): void; -} From 33544c6fcd738e07a9f77793de321ae48d2ed83c Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Tue, 10 Dec 2024 13:26:05 +0200 Subject: [PATCH 08/54] feat: added renderer & tests & dynamic form boilerplate --- .../Form/DynamicForm/DynamicForm.tsx | 3 + .../organisms/Form/DynamicForm/index.ts | 1 + .../organisms/Renderer/Renderer.stories.tsx | 136 +++ .../organisms/Renderer/Renderer.tsx | 49 ++ .../organisms/Renderer/Renderer.unit.test.tsx | 104 +++ .../components/organisms/Renderer/index.ts | 3 + .../components/organisms/Renderer/types.ts | 30 + .../utils/create-rendered-element-key.ts | 4 + .../create-rendered-element-key.unit.test.ts | 32 + .../Renderer/utils/create-test-id.ts | 5 + .../utils/create-test-id.unit.test.ts | 32 + .../organisms/Renderer/utils/index.ts | 2 + packages/ui/tsconfig.json | 2 +- pnpm-lock.yaml | 808 +++++++----------- .../workflows-service/prisma/data-migrations | 2 +- 15 files changed, 713 insertions(+), 500 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/index.ts create mode 100644 packages/ui/src/components/organisms/Renderer/Renderer.stories.tsx create mode 100644 packages/ui/src/components/organisms/Renderer/Renderer.tsx create mode 100644 packages/ui/src/components/organisms/Renderer/Renderer.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Renderer/index.ts create mode 100644 packages/ui/src/components/organisms/Renderer/types.ts create mode 100644 packages/ui/src/components/organisms/Renderer/utils/create-rendered-element-key.ts create mode 100644 packages/ui/src/components/organisms/Renderer/utils/create-rendered-element-key.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Renderer/utils/create-test-id.ts create mode 100644 packages/ui/src/components/organisms/Renderer/utils/create-test-id.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Renderer/utils/index.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx new file mode 100644 index 0000000000..b67908a0fe --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -0,0 +1,3 @@ +export const DynamicForm = () => { + return
DynamicForm
; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/index.ts new file mode 100644 index 0000000000..2a4d8a96c8 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/index.ts @@ -0,0 +1 @@ +export * from './DynamicForm'; diff --git a/packages/ui/src/components/organisms/Renderer/Renderer.stories.tsx b/packages/ui/src/components/organisms/Renderer/Renderer.stories.tsx new file mode 100644 index 0000000000..1c0f782fcd --- /dev/null +++ b/packages/ui/src/components/organisms/Renderer/Renderer.stories.tsx @@ -0,0 +1,136 @@ +import { Meta } from '@storybook/react'; +import { useId } from 'react'; +import { Renderer } from './Renderer'; +import { IRendererComponent, IRendererElement, TRendererSchema } from './types'; +import { createTestId } from './utils/create-test-id'; + +const ContainerComponent: IRendererComponent = ({ + stack, + children, + definition, +}) => { + return ( +
+ {children} +
+ ); +}; + +const Heading: IRendererComponent = ({ + stack, + options, + definition, +}) => { + return ( +

+ {options?.text} +

+ ); +}; + +const TextField: IRendererComponent< + IRendererElement, + any, + { label: string; placeholder: string } +> = ({ stack, options, definition }) => { + const id = useId(); + + return ( +
+ {options?.label && } + +
+ ); +}; + +const schema: TRendererSchema = { + container: ContainerComponent, + heading: Heading, + textfield: TextField, +}; + +export default { + component: Renderer, +} satisfies Meta; + +const plainRendererDefinition: IRendererElement[] = [ + { + id: 'container', + element: 'container', + children: [ + { + id: 'heading', + element: 'heading', + options: { + text: 'Hello World', + }, + }, + { + id: 'text-field', + element: 'textfield', + options: { + label: 'Name', + placeholder: 'Enter your name', + }, + }, + ], + }, +]; + +export const PlainRender = { + render: () => , +}; + +const nestedRenderDefinition: IRendererElement[] = [ + { + id: 'container', + element: 'container', + children: [ + { + id: 'heading', + element: 'heading', + options: { + text: 'Level 1', + }, + }, + { + id: 'sub-children', + element: 'container', + children: [ + { + id: 'sub-heading', + element: 'heading', + options: { + text: 'Level 2', + }, + }, + { + id: 'children-of-sub-children', + element: 'container', + children: [ + { + id: 'sub-sub-heading', + element: 'heading', + options: { + text: 'Level 3', + }, + }, + ], + }, + ], + }, + ], + }, +]; + +export const NestedRender = { + render: () => , +}; diff --git a/packages/ui/src/components/organisms/Renderer/Renderer.tsx b/packages/ui/src/components/organisms/Renderer/Renderer.tsx new file mode 100644 index 0000000000..518c9e072b --- /dev/null +++ b/packages/ui/src/components/organisms/Renderer/Renderer.tsx @@ -0,0 +1,49 @@ +import { IRendererProps } from './types'; +import { createRenderedElementKey } from './utils/create-rendered-element-key'; + +export const Renderer: React.FunctionComponent = ({ + schema, + elements, + stack, +}) => { + return ( + <> + {elements.map((element, index) => { + const Component = schema[element.element]; + + if (!element.element) + throw new Error(`Element name is missing in definition ${JSON.stringify(element)}`); + + // if (!Component) throw new Error(`Component ${element.element} not found in schema.`); + + if (!Component) return null; + + if (element.children) { + return ( + + + + ); + } + + return ( + + ); + })} + + ); +}; diff --git a/packages/ui/src/components/organisms/Renderer/Renderer.unit.test.tsx b/packages/ui/src/components/organisms/Renderer/Renderer.unit.test.tsx new file mode 100644 index 0000000000..a3f714957c --- /dev/null +++ b/packages/ui/src/components/organisms/Renderer/Renderer.unit.test.tsx @@ -0,0 +1,104 @@ +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { Renderer } from './Renderer'; +import { IRendererComponent, IRendererElement } from './types'; +import { createTestId } from './utils'; + +describe('Renderer', () => { + const MockComponent: IRendererComponent = ({ children, stack, definition }) => ( +
{children}
+ ); + + const baseSchema = { + test: MockComponent, + nested: MockComponent, + }; + + it('should render elements without children', () => { + const elements = [ + { id: '1', element: 'test' }, + { id: '2', element: 'test' }, + ]; + + const { container } = render(); + expect(container.querySelectorAll('div')).toHaveLength(2); + }); + + it('should render nested elements with children', () => { + const elements = [ + { + id: '1', + element: 'nested', + children: [{ id: '2', element: 'test' }], + }, + ]; + + const { container } = render(); + expect(container.querySelectorAll('div')).toHaveLength(2); + }); + + it('should throw error when element name is missing', () => { + const elements = [{ id: '1' } as IRendererElement]; + + expect(() => { + render(); + }).toThrow('Element name is missing'); + }); + + it('should return null when component is not found in schema', () => { + const elements = [{ id: '1', element: 'nonexistent' }]; + + const { container } = render(); + expect(container.querySelectorAll('div')).toHaveLength(0); + }); + + it('should pass correct props to components', () => { + const TestComponent = ({ definition, stack, options }: any) => ( +
+ ); + + const schema = { test: TestComponent }; + const elements = [{ id: '1', element: 'test', options: { test: 'value' } }]; + const stack = [0, 1]; + + const { getByTestId } = render(); + + const element = getByTestId('test'); + expect(element.dataset.definition).toBe('1'); + expect(element.dataset.stack).toBe('0,1'); + expect(element.dataset.options).toBe('value'); + }); + + it('should handle deeply nested elements', () => { + const elements = [ + { + id: '1', + element: 'nested', + children: [ + { + id: '2', + element: 'nested', + children: [{ id: '3', element: 'test' }], + }, + ], + }, + ]; + + const { container } = render(); + + const level1 = container.querySelector('[data-testid="1"]') as HTMLElement; + const level2 = container.querySelector('[data-testid="2-0"]') as HTMLElement; + const level3 = container.querySelector('[data-testid="3-0-0"]') as HTMLElement; + expect(level1).toBeDefined(); + expect(level2).toBeDefined(); + expect(level3).toBeDefined(); + + expect(level1.contains(level2)).toBe(true); + expect(level2.contains(level3)).toBe(true); + }); +}); diff --git a/packages/ui/src/components/organisms/Renderer/index.ts b/packages/ui/src/components/organisms/Renderer/index.ts new file mode 100644 index 0000000000..063f8f0777 --- /dev/null +++ b/packages/ui/src/components/organisms/Renderer/index.ts @@ -0,0 +1,3 @@ +export * from './Renderer'; +export * from './types'; +export * from './utils'; diff --git a/packages/ui/src/components/organisms/Renderer/types.ts b/packages/ui/src/components/organisms/Renderer/types.ts new file mode 100644 index 0000000000..ad3ed97efc --- /dev/null +++ b/packages/ui/src/components/organisms/Renderer/types.ts @@ -0,0 +1,30 @@ +export interface IRendererElement { + id: string; + element: string; + children?: IRendererElement[]; + options?: Record; +} + +export type IRendererComponent< + TDefinition extends IRendererElement, + TProps extends Record, + TOptions extends Record = Record, + TBaseProps = { + stack?: number[]; + children?: React.ReactNode | React.ReactNode[]; + options?: TOptions; + definition: TDefinition; + }, +> = React.FunctionComponent; + +export type TRendererElementName = string; + +export type TRendererSchema = Record< + TRendererElementName, + IRendererComponent> +>; + +export interface IRendererProps { + elements: IRendererElement[]; + schema: TRendererSchema; +} diff --git a/packages/ui/src/components/organisms/Renderer/utils/create-rendered-element-key.ts b/packages/ui/src/components/organisms/Renderer/utils/create-rendered-element-key.ts new file mode 100644 index 0000000000..5004d9370e --- /dev/null +++ b/packages/ui/src/components/organisms/Renderer/utils/create-rendered-element-key.ts @@ -0,0 +1,4 @@ +import { IRendererElement } from '@/components/organisms/Renderer/types'; + +export const createRenderedElementKey = (element: IRendererElement, stack?: number[]) => + [element.id, ...(stack || [])].join('-'); diff --git a/packages/ui/src/components/organisms/Renderer/utils/create-rendered-element-key.unit.test.ts b/packages/ui/src/components/organisms/Renderer/utils/create-rendered-element-key.unit.test.ts new file mode 100644 index 0000000000..ea2056e19c --- /dev/null +++ b/packages/ui/src/components/organisms/Renderer/utils/create-rendered-element-key.unit.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { IRendererElement } from '../types'; +import { createRenderedElementKey } from './create-rendered-element-key'; + +describe('createRenderedElementKey', () => { + it('should create key from element id when no stack provided', () => { + const element = { id: 'test-element' } as IRendererElement; + const result = createRenderedElementKey(element); + expect(result).toBe('test-element'); + }); + + it('should create key from element id and stack when stack provided', () => { + const element = { id: 'test-element' } as IRendererElement; + const stack = [1, 2, 3]; + const result = createRenderedElementKey(element, stack); + expect(result).toBe('test-element-1-2-3'); + }); + + it('should handle empty stack array', () => { + const element = { id: 'test-element' } as IRendererElement; + const stack: number[] = []; + const result = createRenderedElementKey(element, stack); + expect(result).toBe('test-element'); + }); + + it('should handle single stack number', () => { + const element = { id: 'test-element' } as IRendererElement; + const stack = [1]; + const result = createRenderedElementKey(element, stack); + expect(result).toBe('test-element-1'); + }); +}); diff --git a/packages/ui/src/components/organisms/Renderer/utils/create-test-id.ts b/packages/ui/src/components/organisms/Renderer/utils/create-test-id.ts new file mode 100644 index 0000000000..e8fa96bdfa --- /dev/null +++ b/packages/ui/src/components/organisms/Renderer/utils/create-test-id.ts @@ -0,0 +1,5 @@ +import { IRendererElement } from '@/components/organisms/Renderer/types'; + +export const createTestId = (definition: IRendererElement, stack?: number[]) => { + return [definition.id, ...(stack || [])].join('-'); +}; diff --git a/packages/ui/src/components/organisms/Renderer/utils/create-test-id.unit.test.ts b/packages/ui/src/components/organisms/Renderer/utils/create-test-id.unit.test.ts new file mode 100644 index 0000000000..23a6d4d11a --- /dev/null +++ b/packages/ui/src/components/organisms/Renderer/utils/create-test-id.unit.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { IRendererElement } from '../types'; +import { createTestId } from './create-test-id'; + +describe('createTestId', () => { + it('should create test id from element id when no stack provided', () => { + const element = { id: 'test-element' } as IRendererElement; + const result = createTestId(element); + expect(result).toBe('test-element'); + }); + + it('should create test id from element id and stack when stack provided', () => { + const element = { id: 'test-element' } as IRendererElement; + const stack = [1, 2, 3]; + const result = createTestId(element, stack); + expect(result).toBe('test-element-1-2-3'); + }); + + it('should handle empty stack array', () => { + const element = { id: 'test-element' } as IRendererElement; + const stack: number[] = []; + const result = createTestId(element, stack); + expect(result).toBe('test-element'); + }); + + it('should handle single stack number', () => { + const element = { id: 'test-element' } as IRendererElement; + const stack = [1]; + const result = createTestId(element, stack); + expect(result).toBe('test-element-1'); + }); +}); diff --git a/packages/ui/src/components/organisms/Renderer/utils/index.ts b/packages/ui/src/components/organisms/Renderer/utils/index.ts new file mode 100644 index 0000000000..002be98a7d --- /dev/null +++ b/packages/ui/src/components/organisms/Renderer/utils/index.ts @@ -0,0 +1,2 @@ +export * from './create-rendered-element-key'; +export * from './create-test-id'; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index f4534119ee..c55b57d833 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -7,7 +7,7 @@ "paths": { "@/*": ["./src/*"] }, - "module": "preserve", + "module": "ESNext", "moduleResolution": "bundler" }, "include": ["src"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9e5c2c26e..6232e3461b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1881,6 +1881,9 @@ importers: '@types/json-logic-js': specifier: ^2.0.1 version: 2.0.5 + '@types/jsoneditor': + specifier: ^9.9.5 + version: 9.9.5 '@types/lodash': specifier: ^4.14.191 version: 4.14.201 @@ -1923,6 +1926,9 @@ importers: fast-glob: specifier: ^3.3.0 version: 3.3.2 + jsoneditor: + specifier: ^10.1.0 + version: 10.1.0 prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -4920,25 +4926,7 @@ packages: '@babel/helper-function-name': 7.23.0 '@babel/helper-member-expression-to-functions': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - semver: 6.3.1 - dev: true - - /@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.25.2): - resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.23.0 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 semver: 6.3.1 @@ -4956,13 +4944,13 @@ packages: semver: 6.3.1 dev: true - /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.25.2): + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.23.7): resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-annotate-as-pure': 7.22.5 regexpu-core: 5.3.2 semver: 6.3.1 @@ -4984,12 +4972,12 @@ packages: - supports-color dev: true - /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.25.2): + /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.23.7): resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 debug: 4.3.6 @@ -5079,20 +5067,6 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true - /@babel/helper-module-transforms@7.23.3(@babel/core@7.25.2): - resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 - dev: true - /@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2): resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} engines: {node: '>=6.9.0'} @@ -5131,13 +5105,13 @@ packages: '@babel/helper-wrap-function': 7.22.20 dev: true - /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.25.2): + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.7): resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-wrap-function': 7.22.20 @@ -5155,13 +5129,13 @@ packages: '@babel/helper-optimise-call-expression': 7.22.5 dev: true - /@babel/helper-replace-supers@7.22.20(@babel/core@7.25.2): + /@babel/helper-replace-supers@7.22.20(@babel/core@7.23.7): resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-member-expression-to-functions': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 @@ -5324,13 +5298,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.25.2): + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5346,25 +5320,25 @@ packages: '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.17.9) dev: true - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.25.2): + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.23.7) dev: true - /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.25.2): + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5569,13 +5543,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2): + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.7): resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 dev: true /@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.17.9): @@ -5622,15 +5596,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.2): - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: @@ -5658,15 +5623,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.2): - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.17.9): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} @@ -5677,13 +5633,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.2): + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.7): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5696,12 +5652,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.25.2): + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5714,12 +5670,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.25.2): + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5733,23 +5689,23 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.25.2): + /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.25.2): + /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5762,15 +5718,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.2): - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: @@ -5789,15 +5736,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.2): - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.17.9): resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} engines: {node: '>=6.9.0'} @@ -5846,15 +5784,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.2): - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: @@ -5873,15 +5802,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.2): - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.17.9): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: @@ -5900,15 +5820,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.2): - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: @@ -5927,15 +5838,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.2): - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: @@ -5954,15 +5856,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.2): - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: @@ -5981,15 +5874,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.2): - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.17.9): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} @@ -6000,13 +5884,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.2): + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.7): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6030,16 +5914,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.2): - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.17.9): resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} engines: {node: '>=6.9.0'} @@ -6060,14 +5934,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.25.2): + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.7): resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6081,27 +5955,27 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-async-generator-functions@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-async-generator-functions@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-59GsVNavGxAXCDDbakWSMJhajASb4kBCqDjqJsv+p5nKdbz7istmZ3HrX3L2LuiI80+zsOADCvooqQH3qGCucQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.25.2) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.7) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.7) dev: true /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.17.9): @@ -6116,16 +5990,16 @@ packages: '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.17.9) dev: true - /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-module-imports': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.25.2) + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.7) dev: true /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.17.9): @@ -6138,13 +6012,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6158,37 +6032,37 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-block-scoping@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-block-scoping@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-QPZxHrThbQia7UdvfpaRRlq/J9ciz1J4go0k+lPBXbgaNeY7IQrBj/9ceWjvMMI07/ZBzHl/F0R/2K0qH7jCVw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-class-static-block@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-class-static-block@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-PENDVxdr7ZxKPyi5Ffc0LjXdnJyrJxyqF5T5YjlVg4a0VFfQHW0r8iAtRiDXkfHlu1wwcvdtnndGYIeJLSuRMQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.7) dev: true /@babel/plugin-transform-classes@7.23.3(@babel/core@7.17.9): @@ -6209,20 +6083,20 @@ packages: globals: 11.12.0 dev: true - /@babel/plugin-transform-classes@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-classes@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) '@babel/helper-split-export-declaration': 7.22.6 globals: 11.12.0 dev: true @@ -6238,13 +6112,13 @@ packages: '@babel/template': 7.22.15 dev: true - /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 '@babel/template': 7.22.15 dev: true @@ -6259,13 +6133,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6280,14 +6154,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6301,25 +6175,25 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-dynamic-import@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-dynamic-import@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-vTG+cTGxPFou12Rj7ll+eD5yWeNl5/8xvQvF08y5Gv3v4mZQoyFf8/n9zg4q5vvCWt5jmgymfzMAldO7orBn7A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.7) dev: true /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.17.9): @@ -6333,26 +6207,26 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-export-namespace-from@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-export-namespace-from@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-yCLhW34wpJWRdTxxWtFZASJisihrfyMOTOQexhVzA78jlU+dH7Dw+zQgcPepQ5F3C6bAIiblZZ+qBggJdHiBAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.7) dev: true /@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.23.7): @@ -6376,13 +6250,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-for-of@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-for-of@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6398,27 +6272,27 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-function-name': 7.23.0 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-json-strings@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-json-strings@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-H9Ej2OiISIZowZHaBwF0tsJOih1PftXJtE8EWqlEIwpc7LMTGq0rPOrywKLQ4nefzx8/HMR0D3JGXoMHYvhi0A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.7) dev: true /@babel/plugin-transform-literals@7.23.3(@babel/core@7.17.9): @@ -6431,25 +6305,25 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-literals@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-literals@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-logical-assignment-operators@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-logical-assignment-operators@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-+pD5ZbxofyOygEp+zZAfujY2ShNCXRpDRIPOiBmTO693hhyOEteZgl876Xs9SAHPQpcV0vz8LvA/T+w8AzyX8A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.7) dev: true /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.17.9): @@ -6462,13 +6336,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6483,14 +6357,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6513,19 +6387,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.23.7 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-simple-access': 7.22.5 - dev: true - - /@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.25.2): - resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-simple-access': 7.22.5 dev: true @@ -6543,17 +6405,17 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true - /@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-identifier': 7.24.7 + '@babel/helper-validator-identifier': 7.22.20 dev: true /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.17.9): @@ -6567,14 +6429,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6589,14 +6451,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.25.2): + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.23.7): resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6610,50 +6472,50 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-nullish-coalescing-operator@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-nullish-coalescing-operator@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-xzg24Lnld4DYIdysyf07zJ1P+iIfJpxtVFOzX4g+bsJ3Ng5Le7rXx9KwqKzuyaUeRnt+I1EICwQITqc0E2PmpA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.7) dev: true - /@babel/plugin-transform-numeric-separator@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-numeric-separator@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-s9GO7fIBi/BLsZ0v3Rftr6Oe4t0ctJ8h4CCXfPoEJwmvAPMyNrfkOOJzm6b9PX9YXcCJWWQd/sBF/N26eBiMVw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.7) dev: true - /@babel/plugin-transform-object-rest-spread@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-object-rest-spread@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-VxHt0ANkDmu8TANdE9Kc0rndo/ccsmfe2Cx2y5sI4hu3AukHQ5wAu4cM7j3ba8B9548ijVyclBU+nuDQftZsog==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/compat-data': 7.23.5 - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.7) dev: true /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.17.9): @@ -6667,26 +6529,26 @@ packages: '@babel/helper-replace-supers': 7.22.20(@babel/core@7.17.9) dev: true - /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) dev: true - /@babel/plugin-transform-optional-catch-binding@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-optional-catch-binding@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-LxYSb0iLjUamfm7f1D7GpiS4j0UAC8AOiehnsGAP8BEsIX8EOi3qV6bbctw8M7ZvLtcoZfZX5Z7rN9PlWk0m5A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.7) dev: true /@babel/plugin-transform-optional-chaining@7.23.3(@babel/core@7.17.9): @@ -6701,16 +6563,16 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.17.9) dev: true - /@babel/plugin-transform-optional-chaining@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-optional-chaining@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-zvL8vIfIUgMccIAK1lxjvNv572JHFJIKb4MWBz5OGdBQA0fB0Xluix5rmOby48exiJc987neOmP/m9Fnpkz3Tg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.7) dev: true /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.17.9): @@ -6723,38 +6585,38 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-private-property-in-object@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-private-property-in-object@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-a5m2oLNFyje2e/rGKjVfAELTVI5mbA0FeZpBnkOWWV7eSmKQ+T/XW0Vf+29ScLzSxX+rnsarvU0oie/4m6hkxA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.7) dev: true /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.17.9): @@ -6767,13 +6629,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6887,13 +6749,13 @@ packages: regenerator-transform: 0.15.2 dev: true - /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 regenerator-transform: 0.15.2 dev: true @@ -6908,13 +6770,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6928,13 +6790,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6949,13 +6811,13 @@ packages: '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true - /@babel/plugin-transform-spread@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-spread@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true @@ -6970,13 +6832,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6990,13 +6852,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -7010,13 +6872,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -7056,24 +6918,24 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -7088,25 +6950,25 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.25.2): + /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -7206,171 +7068,80 @@ packages: '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.2) - '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-async-generator-functions': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-block-scoping': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-class-static-block': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-classes': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-dynamic-import': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-export-namespace-from': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-json-strings': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-logical-assignment-operators': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.25.2) - '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-nullish-coalescing-operator': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-numeric-separator': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-object-rest-spread': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-optional-catch-binding': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-private-property-in-object': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.25.2) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.2) - babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.25.2) - babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.25.2) - babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.25.2) - core-js-compat: 3.33.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/preset-env@7.23.3(@babel/core@7.25.2): - resolution: {integrity: sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.23.5 - '@babel/core': 7.25.2 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.2) - '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-async-generator-functions': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-block-scoping': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-class-static-block': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-classes': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-dynamic-import': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-export-namespace-from': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-json-strings': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-logical-assignment-operators': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.25.2) - '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-nullish-coalescing-operator': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-numeric-separator': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-object-rest-spread': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-optional-catch-binding': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-private-property-in-object': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.25.2) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.2) - babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.25.2) - babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.25.2) - babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.7) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.7) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.23.7) + '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-async-generator-functions': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-block-scoping': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-class-static-block': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-classes': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-dynamic-import': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-export-namespace-from': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-json-strings': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-logical-assignment-operators': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.23.7) + '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-numeric-separator': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-object-rest-spread': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-optional-catch-binding': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-private-property-in-object': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.23.7) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.23.7) + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.23.7) + babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.23.7) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.23.7) core-js-compat: 3.33.2 semver: 6.3.1 transitivePeerDependencies: @@ -7402,12 +7173,12 @@ packages: esutils: 2.0.3 dev: true - /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.25.2): + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.23.7): resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 '@babel/types': 7.23.6 esutils: 2.0.3 @@ -7672,9 +7443,9 @@ packages: eslint-plugin-prefer-arrow: ^1.2.3 eslint-plugin-unused-imports: ^3.0.0 dependencies: - '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@5.5.4) - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.5.4) - '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.5.4) + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@5.1.6) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.1.6) + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.1.6) eslint: 8.54.0 eslint-config-prettier: 9.0.0(eslint@8.54.0) eslint-plugin-prefer-arrow: 1.2.3(eslint@8.54.0) @@ -11227,7 +10998,7 @@ packages: '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) '@mui/base': 5.0.0-beta.24(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@mui/core-downloads-tracker': 5.14.18 - '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) '@mui/types': 7.2.13(@types/react@18.2.37) '@mui/utils': 5.15.6(@types/react@18.2.37)(react@18.2.0) '@types/react': 18.2.37 @@ -11297,6 +11068,27 @@ packages: react: 18.2.0 dev: false + /@mui/styled-engine@5.15.6(@emotion/react@11.11.1)(react@18.2.0): + resolution: {integrity: sha512-KAn8P8xP/WigFKMlEYUpU9z2o7jJnv0BG28Qu1dhNQVutsLVIFdRf5Nb+0ijp2qgtcmygQ0FtfRuXv5LYetZTg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@mui/system@5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-J01D//u8IfXvaEHMBQX5aO2l7Q+P15nt96c4NskX7yp5/+UuZP8XCQJhtBtLuj+M2LLyXHYGmCPeblsmmscP2Q==} engines: {node: '>=12.0.0'} @@ -11347,7 +11139,7 @@ packages: '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0) '@mui/private-theming': 5.15.6(@types/react@18.2.43)(react@18.2.0) - '@mui/styled-engine': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/styled-engine': 5.15.6(@emotion/react@11.11.1)(react@18.2.0) '@mui/types': 7.2.13(@types/react@18.2.43) '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 @@ -11357,6 +11149,35 @@ packages: react: 18.2.0 dev: false + /@mui/system@5.15.6(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0): + resolution: {integrity: sha512-J01D//u8IfXvaEHMBQX5aO2l7Q+P15nt96c4NskX7yp5/+UuZP8XCQJhtBtLuj+M2LLyXHYGmCPeblsmmscP2Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) + '@mui/private-theming': 5.15.6(@types/react@18.2.37)(react@18.2.0) + '@mui/styled-engine': 5.15.6(@emotion/react@11.11.1)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.37) + '@mui/utils': 5.15.6(@types/react@18.2.37)(react@18.2.0) + '@types/react': 18.2.37 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@mui/types@7.2.13(@types/react@18.2.37): resolution: {integrity: sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==} peerDependencies: @@ -11457,10 +11278,10 @@ packages: '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) '@mui/base': 5.0.0-beta.24(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@mui/material': 5.14.18(@emotion/react@11.11.1)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) '@mui/utils': 5.15.6(@types/react@18.2.37)(react@18.2.0) '@types/react-transition-group': 4.4.9 - clsx: 2.1.0 + clsx: 2.1.1 date-fns: 3.6.0 dayjs: 1.11.10 prop-types: 15.8.1 @@ -11571,7 +11392,7 @@ packages: '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react@18.2.0) '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) '@types/react-transition-group': 4.4.9 - clsx: 2.1.0 + clsx: 2.1.1 dayjs: 1.11.10 prop-types: 15.8.1 react: 18.2.0 @@ -16254,7 +16075,6 @@ packages: /@sphinxxxx/color-conversion@2.2.2: resolution: {integrity: sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==} - dev: false /@storybook/addon-a11y@6.5.16(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-/e9s34o+TmEhy+Q3/YzbRJ5AJ/Sy0gjZXlvsCrcRpiQLdt5JRbN8s+Lbn/FWxy8U1Tb1wlLYlqjJ+fYi5RrS3A==} @@ -17562,7 +17382,7 @@ packages: hasBin: true dependencies: '@babel/core': 7.23.7 - '@babel/preset-env': 7.23.3(@babel/core@7.25.2) + '@babel/preset-env': 7.23.3(@babel/core@7.23.7) '@babel/types': 7.23.6 '@ndelangen/get-tarball': 3.0.9 '@storybook/codemod': 7.5.3 @@ -22613,7 +22433,6 @@ packages: /ace-builds@1.35.4: resolution: {integrity: sha512-r0KQclhZ/uk5a4zOqRYQkJuQuu4vFMiA6VTj54Tk4nI1TUR3iEMMppZkWbNoWEgWwv4ciDloObb9Rf4V55Qgjw==} - dev: false /acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} @@ -23433,14 +23252,14 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.25.2): + /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.23.7): resolution: {integrity: sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: '@babel/compat-data': 7.23.5 - '@babel/core': 7.25.2 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.7) semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -23458,13 +23277,13 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.25.2): + /babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.23.7): resolution: {integrity: sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.7) core-js-compat: 3.33.2 transitivePeerDependencies: - supports-color @@ -23481,13 +23300,13 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.25.2): + /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.23.7): resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.25.2 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) + '@babel/core': 7.23.7 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.7) transitivePeerDependencies: - supports-color dev: true @@ -27567,7 +27386,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.22.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.22.0)(typescript@4.9.5) eslint: 8.22.0 eslint-rule-composer: 0.3.0 dev: true @@ -27597,7 +27416,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.1.6) eslint: 8.54.0 eslint-rule-composer: 0.3.0 dev: true @@ -30607,7 +30426,6 @@ packages: /javascript-natural-sort@0.7.1: resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} - dev: false /jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} @@ -31380,7 +31198,6 @@ packages: /json-source-map@0.6.1: resolution: {integrity: sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==} - dev: false /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -31426,7 +31243,6 @@ packages: mobius1-selectr: 2.4.13 picomodal: 3.0.0 vanilla-picker: 2.12.3 - dev: false /jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -31459,7 +31275,6 @@ packages: /jsonrepair@3.8.0: resolution: {integrity: sha512-89lrxpwp+IEcJ6kwglF0HH3Tl17J08JEpYfXnvvjdp4zV4rjSoGu2NdQHxBs7yTOk3ETjTn9du48pBy8iBqj1w==} hasBin: true - dev: false /jsonwebtoken@9.0.0: resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==} @@ -33367,7 +33182,6 @@ packages: /mobius1-selectr@2.4.13: resolution: {integrity: sha512-Mk9qDrvU44UUL0EBhbAA1phfQZ7aMZPjwtL7wkpiBzGh8dETGqfsh50mWoX9EkjDlkONlErWXArHCKfoxVg0Bw==} - dev: false /moment@2.29.4: resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} @@ -34538,7 +34352,6 @@ packages: /picomodal@3.0.0: resolution: {integrity: sha512-FoR3TDfuLlqUvcEeK5ifpKSVVns6B4BQvc8SDF6THVMuadya6LLtji0QgUDSStw0ZR2J7I6UGi5V2V23rnPWTw==} - dev: false /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} @@ -39872,7 +39685,6 @@ packages: resolution: {integrity: sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==} dependencies: '@sphinxxxx/color-conversion': 2.2.2 - dev: false /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations index 7c197ae8dc..15f349319b 160000 --- a/services/workflows-service/prisma/data-migrations +++ b/services/workflows-service/prisma/data-migrations @@ -1 +1 @@ -Subproject commit 7c197ae8dc1d459e4049ad01d86cfb4e7029bb53 +Subproject commit 15f349319b8bd23f34a0e6ca28ed190e0a5bab86 From 4375b7d25771efea8c9df86cdec230da49095b79 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Tue, 10 Dec 2024 16:31:55 +0200 Subject: [PATCH 09/54] feat: added rule engine --- packages/ui/package.json | 3 + .../fields/FieldList/FieldList.tsx | 48 ++ .../FieldList/hooks/useFieldList/index.ts | 1 + .../hooks/useFieldList/useFieldList.ts | 49 ++ .../DynamicForm/fields/FieldList/index.ts | 1 + .../providers/StackProvider/StackProvider.tsx | 13 + .../context/stack-provider-context.ts | 6 + .../StackProvider/hooks/useStack/index.ts | 1 + .../StackProvider/hooks/useStack/useStack.ts | 4 + .../providers/StackProvider/index.ts | 2 + .../providers/StackProvider/types/index.ts | 3 + .../organisms/Form/DynamicForm/types/index.ts | 13 + .../useRuleEngine/engines/json-logic/index.ts | 1 + .../engines/json-logic/json-logic.ts | 17 + .../json-logic/json-logic.unit.test.ts | 58 ++ .../engines/json-schema/index.ts | 1 + .../engines/json-schema/json-schema.ts | 37 + .../json-schema/json-schema.unit.test.ts | 112 +++ .../Form/hooks/useRuleEngine/index.ts | 3 + .../useRuleEngine/rule-engine.repository.ts | 29 + .../rule-engine.repository.unit.test.ts | 74 ++ .../Form/hooks/useRuleEngine/types.ts | 13 + .../Form/hooks/useRuleEngine/useRuleEngine.ts | 56 ++ .../useRuleEngine/useRuleEngine.unit.test.ts | 113 +++ .../utils/execute-rule/execute-rule.ts | 8 + .../execute-rule/execute-rule.unit.test.ts | 43 + .../useRuleEngine/utils/execute-rule/index.ts | 1 + .../utils/execute-rules/execute-rules.ts | 9 + .../execute-rules/execute-rules.unit.test.ts | 36 + .../utils/execute-rules/index.ts | 1 + .../organisms/Renderer/Renderer.tsx | 6 +- pnpm-lock.yaml | 812 +++++++++++------- 32 files changed, 1267 insertions(+), 307 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/context/stack-provider-context.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/types/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/index.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/json-logic.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/json-logic.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/index.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/json-schema.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/json-schema.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/index.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/rule-engine.repository.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/rule-engine.repository.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/types.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/execute-rule.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/execute-rule.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/index.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/execute-rules.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/execute-rules.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/index.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index fbff7a5523..d8b1f175aa 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -47,6 +47,9 @@ "@rjsf/utils": "^5.9.0", "@rjsf/validator-ajv8": "^5.9.0", "@tanstack/react-table": "^8.9.2", + "ajv": "^8.12.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", "cmdk": "^0.2.0", diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx new file mode 100644 index 0000000000..4e37a4ca88 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx @@ -0,0 +1,48 @@ +import { useFieldList } from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/hooks/useFieldList'; +import { + StackProvider, + useStack, +} from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider'; +import { rendererSchema } from '@/pages/CollectionFlowV2/renderer-schema'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { AnyObject, Button, Renderer } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export type TFieldListValueType = T[]; + +export interface IFieldListOptions { + defaultValue: AnyObject; + addButtonLabel?: string; + removeButtonLabel?: string; +} + +export const FieldList: FunctionComponent< + IFieldComponentProps, IFieldListOptions> +> = props => { + const { stack } = useStack(); + const { definition, options } = props; + const { addButtonLabel = 'Add Item', removeButtonLabel = 'Remove' } = options || {}; + const { items, addItem, removeItem } = useFieldList(props); + + return ( +
+ {items.map((item, index) => { + return ( +
+
+ removeItem(index)}> + {removeButtonLabel} + +
+ + + +
+ ); + })} +
+ +
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/index.ts new file mode 100644 index 0000000000..dbf39b30fa --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/index.ts @@ -0,0 +1 @@ +export * from './useFieldList'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts new file mode 100644 index 0000000000..b467719a58 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts @@ -0,0 +1,49 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { + IFieldListOptions, + TFieldListValueType, +} from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/FieldList'; +import { useUIElement } from '@/pages/CollectionFlowV2/hooks/useUIElement'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import set from 'lodash/set'; +import { useCallback, useMemo } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +export const useFieldList = ({ + definition, + stack, + fieldProps, + options, +}: IFieldComponentProps, IFieldListOptions>) => { + const { payload, stateApi } = useStateManagerContext(); + const uiElement = useUIElement(definition, payload, stack); + + const items = useMemo(() => (uiElement.getValue() as { _id: string }[]) || [], [uiElement]); + + const addItem = useCallback(() => { + const valueDestination = uiElement.getValueDestination(); + + const newValue = [...items, { _id: uuidv4(), ...options?.defaultValue }]; + set(payload, valueDestination, newValue); + + stateApi.setContext(payload); + }, [uiElement, items, stateApi]); + + const removeItem = useCallback( + (index: number) => { + if (!Array.isArray(items)) return; + + const newValue = items.filter((_, i) => i !== index); + set(payload, uiElement.getValueDestination(), newValue); + + stateApi.setContext(payload); + }, + [uiElement, items, stateApi], + ); + + return { + items, + addItem, + removeItem, + }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/index.ts new file mode 100644 index 0000000000..9fb4cd2987 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/index.ts @@ -0,0 +1 @@ +export * from './FieldList'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.tsx new file mode 100644 index 0000000000..e5339bbad3 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.tsx @@ -0,0 +1,13 @@ +import { FunctionComponent, useMemo } from 'react'; +import { StackProviderContext } from './context/stack-provider-context'; + +export interface IStackProviderProps { + stack?: number[]; + children: React.ReactNode | React.ReactNode[]; +} + +export const StackProvider: FunctionComponent = ({ stack, children }) => { + const context = useMemo(() => ({ stack }), [stack]); + + return {children}; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/context/stack-provider-context.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/context/stack-provider-context.ts new file mode 100644 index 0000000000..970d1d4200 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/context/stack-provider-context.ts @@ -0,0 +1,6 @@ +import { createContext } from 'react'; +import { IStackProviderContext } from '../types'; + +export const StackProviderContext = createContext({ + stack: [], +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/index.ts new file mode 100644 index 0000000000..9c9318428b --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/index.ts @@ -0,0 +1 @@ +export * from './useStack'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.ts new file mode 100644 index 0000000000..3a8bcbda58 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { StackProviderContext } from '../../context/stack-provider-context'; + +export const useStack = () => useContext(StackProviderContext); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/index.ts new file mode 100644 index 0000000000..c7972e4730 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/index.ts @@ -0,0 +1,2 @@ +export * from './hooks/useStack'; +export * from './StackProvider'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/types/index.ts new file mode 100644 index 0000000000..ae6896c9a7 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/types/index.ts @@ -0,0 +1,3 @@ +export interface IStackProviderContext { + stack?: number[]; +} diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts new file mode 100644 index 0000000000..608f922429 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts @@ -0,0 +1,13 @@ +import { IValidationSchema } from '../../Validator'; + +export type TBaseFormElements = 'textinput'; + +export interface IFormElement { + valueDestination: string; + element: TElements; + validate?: IValidationSchema[]; +} + +export interface IDynamicFormProps { + values: TValues; +} diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/index.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/index.ts new file mode 100644 index 0000000000..b48580dffa --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/index.ts @@ -0,0 +1 @@ +export * from './json-logic'; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/json-logic.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/json-logic.ts new file mode 100644 index 0000000000..af9ee39f13 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/json-logic.ts @@ -0,0 +1,17 @@ +import jsonLogic from 'json-logic-js'; +import { IRule, TRuleEngineRunner } from '../../types'; + +export const jsonLogicEngineRunner: TRuleEngineRunner = (context: object, rule: IRule) => { + if (typeof rule.value !== 'object' || rule.value === null) { + throw new Error('JsonLogicEngineRunner: Rule value must be an object'); + } + + const result = jsonLogic.apply(rule.value, context); + + if (typeof result !== 'boolean') { + console.warn('JsonLogicEngineRunner: Rule result is not a boolean', result); + console.warn('Result will be converted to boolean'); + } + + return Boolean(result); +}; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/json-logic.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/json-logic.unit.test.ts new file mode 100644 index 0000000000..fe36674bf8 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-logic/json-logic.unit.test.ts @@ -0,0 +1,58 @@ +import jsonLogic from 'json-logic-js'; +import { describe, expect, it, vi } from 'vitest'; +import { TRuleEngine } from '../../types'; +import { jsonLogicEngineRunner } from './json-logic'; + +vi.mock('json-logic-js', () => ({ + default: { + apply: vi.fn(), + }, +})); + +describe('jsonLogicEngineRunner', () => { + const mockContext = { foo: 'bar' }; + const mockRule = { + engine: 'json-logic' as TRuleEngine, + value: { some: 'logic' }, + }; + + it('should throw error if rule value is not an object', () => { + expect(() => + jsonLogicEngineRunner(mockContext, { ...mockRule, value: 'not-an-object' }), + ).toThrow('JsonLogicEngineRunner: Rule value must be an object'); + + expect(() => jsonLogicEngineRunner(mockContext, { ...mockRule, value: null })).toThrow( + 'JsonLogicEngineRunner: Rule value must be an object', + ); + }); + + it('should call jsonLogic.apply with correct parameters', () => { + jsonLogicEngineRunner(mockContext, mockRule); + expect(jsonLogic.apply).toHaveBeenCalledWith(mockRule.value, mockContext); + }); + + it('should return true when jsonLogic returns truthy value', () => { + vi.mocked(jsonLogic.apply).mockReturnValue(1); + const result = jsonLogicEngineRunner(mockContext, mockRule); + expect(result).toBe(true); + }); + + it('should return false when jsonLogic returns falsy value', () => { + vi.mocked(jsonLogic.apply).mockReturnValue(0); + const result = jsonLogicEngineRunner(mockContext, mockRule); + expect(result).toBe(false); + }); + + it('should log warning when result is not boolean', () => { + const consoleSpy = vi.spyOn(console, 'warn'); + vi.mocked(jsonLogic.apply).mockReturnValue(1); + + jsonLogicEngineRunner(mockContext, mockRule); + + expect(consoleSpy).toHaveBeenCalledWith( + 'JsonLogicEngineRunner: Rule result is not a boolean', + 1, + ); + expect(consoleSpy).toHaveBeenCalledWith('Result will be converted to boolean'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/index.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/index.ts new file mode 100644 index 0000000000..ae1791a04a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/index.ts @@ -0,0 +1 @@ +export * from './json-schema'; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/json-schema.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/json-schema.ts new file mode 100644 index 0000000000..b4f3e80692 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/json-schema.ts @@ -0,0 +1,37 @@ +import ajvErrors from 'ajv-errors'; +import addFormats, { FormatName } from 'ajv-formats'; +import Ajv from 'ajv/dist/2019'; +import { IRule, TRuleEngine, TRuleEngineRunner } from '../../types'; + +const defaultFormats: FormatName[] = ['email', 'uri', 'date', 'date-time']; + +export interface IJsonSchemaRuleEngineParams { + formats?: FormatName[]; + keywords?: boolean; + allErrors?: boolean; + useDefaults?: boolean; +} + +export const jsonSchemaEngineRunner: TRuleEngineRunner = ( + context: object, + rule: IRule, +) => { + if (!rule.value || typeof rule.value !== 'object') { + throw new Error('JsonSchemaEngineRunner: Rule value must be an object'); + } + + const { + formats = defaultFormats, + allErrors = true, + useDefaults = true, + keywords = true, + } = rule.params || {}; + + const validator = new Ajv({ allErrors, useDefaults, validateFormats: false }); + addFormats(validator, { formats, keywords }); + ajvErrors(validator, { singleError: true }); + + const isValid = validator.validate(rule.value, context); + + return Boolean(isValid); +}; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/json-schema.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/json-schema.unit.test.ts new file mode 100644 index 0000000000..5bf00e81d1 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/engines/json-schema/json-schema.unit.test.ts @@ -0,0 +1,112 @@ +import ajvErrors from 'ajv-errors'; +import addFormats from 'ajv-formats'; +import Ajv from 'ajv/dist/2019'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { TRuleEngine } from '../../types'; +import { jsonSchemaEngineRunner } from './json-schema'; + +vi.mock('ajv/dist/2019'); +vi.mock('ajv-formats'); +vi.mock('ajv-errors'); + +describe('jsonSchemaEngineRunner', () => { + const mockContext = { foo: 'bar' }; + const mockRule = { + engine: 'json-schema' as TRuleEngine, + value: { type: 'object' }, + }; + + const mockValidate = vi.fn(); + const mockAjv = { validate: mockValidate }; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(Ajv).mockImplementation(() => mockAjv as any); + vi.mocked(addFormats).mockImplementation(() => undefined as any); + vi.mocked(ajvErrors).mockImplementation(() => undefined as any); + }); + + it('should throw error if rule value is not an object', () => { + expect(() => + jsonSchemaEngineRunner(mockContext, { ...mockRule, value: 'not-an-object' }), + ).toThrow('JsonSchemaEngineRunner: Rule value must be an object'); + + expect(() => jsonSchemaEngineRunner(mockContext, { ...mockRule, value: null })).toThrow( + 'JsonSchemaEngineRunner: Rule value must be an object', + ); + }); + + it('should initialize Ajv with correct default parameters', () => { + jsonSchemaEngineRunner(mockContext, mockRule); + + expect(Ajv).toHaveBeenCalledWith({ + allErrors: true, + useDefaults: true, + validateFormats: false, + }); + }); + + it('should initialize Ajv with custom parameters', () => { + const customRule = { + ...mockRule, + params: { + allErrors: false, + useDefaults: false, + }, + }; + + jsonSchemaEngineRunner(mockContext, customRule); + + expect(Ajv).toHaveBeenCalledWith({ + allErrors: false, + useDefaults: false, + validateFormats: false, + }); + }); + + it('should add formats with default formats', () => { + jsonSchemaEngineRunner(mockContext, mockRule); + + expect(addFormats).toHaveBeenCalledWith(mockAjv, { + formats: ['email', 'uri', 'date', 'date-time'], + keywords: true, + }); + }); + + it('should add formats with custom formats', () => { + const customRule = { + ...mockRule, + params: { + formats: ['email'], + keywords: false, + }, + }; + + jsonSchemaEngineRunner(mockContext, customRule); + + expect(addFormats).toHaveBeenCalledWith(mockAjv, { + formats: ['email'], + keywords: false, + }); + }); + + it('should initialize ajv-errors with singleError option', () => { + jsonSchemaEngineRunner(mockContext, mockRule); + + expect(ajvErrors).toHaveBeenCalledWith(mockAjv, { singleError: true }); + }); + + it('should return true when validation passes', () => { + mockValidate.mockReturnValue(true); + const result = jsonSchemaEngineRunner(mockContext, mockRule); + expect(result).toBe(true); + expect(mockValidate).toHaveBeenCalledWith(mockRule.value, mockContext); + }); + + it('should return false when validation fails', () => { + mockValidate.mockReturnValue(false); + const result = jsonSchemaEngineRunner(mockContext, mockRule); + expect(result).toBe(false); + expect(mockValidate).toHaveBeenCalledWith(mockRule.value, mockContext); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/index.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/index.ts new file mode 100644 index 0000000000..60bdbfd3e8 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/index.ts @@ -0,0 +1,3 @@ +export * from './rule-engine.repository'; +export * from './types'; +export * from './useRuleEngine'; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/rule-engine.repository.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/rule-engine.repository.ts new file mode 100644 index 0000000000..b89d6323a4 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/rule-engine.repository.ts @@ -0,0 +1,29 @@ +import { jsonLogicEngineRunner } from './engines/json-logic/json-logic'; +import { jsonSchemaEngineRunner } from './engines/json-schema/json-schema'; +import { TRuleEngine, TRuleEngineRunner } from './types'; + +export const ruleEngineRepository: Record = { + 'json-logic': jsonLogicEngineRunner, + 'json-schema': jsonSchemaEngineRunner, +}; + +export const getRuleEngineRunner = (engine: TRuleEngines) => { + const runner = ruleEngineRepository[engine as keyof typeof ruleEngineRepository]; + + if (!runner) { + throw new Error(`Rule engine ${engine} not found`); + } + + return runner; +}; + +export const addRuleEngineRunner = ( + engine: TRuleEngines, + runner: TRuleEngineRunner, +) => { + ruleEngineRepository[engine as keyof typeof ruleEngineRepository] = runner; +}; + +export const removeRuleEngineRunner = (engine: TRuleEngines) => { + delete ruleEngineRepository[engine as keyof typeof ruleEngineRepository]; +}; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/rule-engine.repository.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/rule-engine.repository.unit.test.ts new file mode 100644 index 0000000000..1802b77eb2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/rule-engine.repository.unit.test.ts @@ -0,0 +1,74 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { jsonLogicEngineRunner } from './engines/json-logic/json-logic'; +import { jsonSchemaEngineRunner } from './engines/json-schema/json-schema'; +import { + addRuleEngineRunner, + getRuleEngineRunner, + removeRuleEngineRunner, + ruleEngineRepository, +} from './rule-engine.repository'; +import { TRuleEngineRunner } from './types'; + +describe('rule-engine.repository', () => { + describe('getRuleEngineRunner', () => { + it('should return json-logic runner when json-logic engine is requested', () => { + const runner = getRuleEngineRunner('json-logic'); + expect(runner).toBe(jsonLogicEngineRunner); + }); + + it('should return json-schema runner when json-schema engine is requested', () => { + const runner = getRuleEngineRunner('json-schema'); + expect(runner).toBe(jsonSchemaEngineRunner); + }); + + it('should throw error when requesting non-existent engine', () => { + expect(() => getRuleEngineRunner('non-existent-engine' as any)).toThrow( + 'Rule engine non-existent-engine not found', + ); + }); + }); + + describe('addRuleEngineRunner', () => { + const mockRunner: TRuleEngineRunner = vi.fn(); + + afterEach(() => { + // Clean up added runners + delete ruleEngineRepository['custom-engine' as keyof typeof ruleEngineRepository]; + }); + + it('should add new engine runner to repository', () => { + addRuleEngineRunner('custom-engine', mockRunner); + expect(getRuleEngineRunner('custom-engine')).toBe(mockRunner); + }); + + it('should override existing engine runner', () => { + const originalRunner = getRuleEngineRunner('json-logic'); + const newMockRunner: TRuleEngineRunner = vi.fn(); + + addRuleEngineRunner('json-logic', newMockRunner); + expect(getRuleEngineRunner('json-logic')).toBe(newMockRunner); + + // Restore original runner + addRuleEngineRunner('json-logic', originalRunner); + }); + }); + + describe('removeRuleEngineRunner', () => { + const mockRunner: TRuleEngineRunner = vi.fn(); + + beforeEach(() => { + addRuleEngineRunner('custom-engine', mockRunner); + }); + + it('should remove engine runner from repository', () => { + removeRuleEngineRunner('custom-engine'); + expect(() => getRuleEngineRunner('custom-engine')).toThrow( + 'Rule engine custom-engine not found', + ); + }); + + it('should not throw when removing non-existent engine', () => { + expect(() => removeRuleEngineRunner('non-existent-engine')).not.toThrow(); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/types.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/types.ts new file mode 100644 index 0000000000..f86ecf9198 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/types.ts @@ -0,0 +1,13 @@ +export type TRuleEngineRunner = (context: object, rule: IRule) => boolean; +export type TRuleEngine = 'json-logic' | 'json-schema'; + +export interface IRule { + engine: TRuleEngines; + value: unknown; + params?: TParams; +} + +export interface IRuleExecutionResult { + rule: IRule; + result: boolean; +} diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.ts new file mode 100644 index 0000000000..267ac0cfae --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.ts @@ -0,0 +1,56 @@ +import debounce from 'lodash/debounce'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { IRule, IRuleExecutionResult, TRuleEngine } from './types'; +import { executeRules } from './utils/execute-rules'; + +export interface IRuleEngineParams { + rules: Array> | IRule; + executeRulesSync?: boolean; + runOnInitialize?: boolean; + executionDelay?: number; +} + +export const useRuleEngine = ( + context: object, + params: IRuleEngineParams, +): IRuleExecutionResult[] => { + const { executeRulesSync, rules: _rules, runOnInitialize = false, executionDelay = 500 } = params; + + const [asyncRuleEngineExecutionResults, setAsyncRuleEngineExecutionResults] = useState< + IRuleExecutionResult[] + >(() => + runOnInitialize && !executeRulesSync + ? executeRules(context, Array.isArray(_rules) ? _rules : [_rules]) + : [], + ); + + const rules = useMemo(() => (Array.isArray(_rules) ? _rules : [_rules]), [_rules]); + + const syncRuleEngineExecutionResults = useMemo(() => { + if (!executeRulesSync) return []; + + const results = executeRules(context, rules); + console.log('Executed rules synchronously', results); + + return results; + }, [rules, context, executeRulesSync]); + + const executeRulesDebounced = useCallback( + debounce((context: object, rules: Array>) => { + const results = executeRules(context, rules); + + console.log('Executed rules asynchronously', results); + + setAsyncRuleEngineExecutionResults(results); + }, executionDelay), + [executionDelay], + ); + + useEffect(() => { + if (executeRulesSync) return; + + executeRulesDebounced(context, rules); + }, [context, rules, executeRulesSync, executeRulesDebounced]); + + return executeRulesSync ? syncRuleEngineExecutionResults : asyncRuleEngineExecutionResults; +}; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts new file mode 100644 index 0000000000..234d886085 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts @@ -0,0 +1,113 @@ +import { renderHook } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { useRuleEngine } from './useRuleEngine'; +import { executeRules } from './utils/execute-rules'; + +vi.mock('./utils/execute-rules', () => ({ + executeRules: vi.fn(), +})); + +describe('useRuleEngine', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should execute rules synchronously when executeRulesSync is true', () => { + // Arrange + const context = { foo: 'bar' }; + const rules = [{ engine: 'json-logic', value: true }]; + const expectedResults = [{ rule: rules[0], result: true }]; + + (executeRules as Mock).mockReturnValue(expectedResults); + + // Act + const { result } = renderHook(() => useRuleEngine(context, { rules, executeRulesSync: true })); + + // Assert + expect(result.current).toEqual(expectedResults); + expect(executeRules).toHaveBeenCalledWith(context, rules); + }); + + it('should execute rules asynchronously when executeRulesSync is false', async () => { + // Arrange + const context = { foo: 'bar' }; + const rules = [{ engine: 'json-logic', value: true }]; + const expectedResults = [{ rule: rules[0], result: true }]; + + (executeRules as Mock).mockReturnValue(expectedResults); + + // Act + const { result } = renderHook(() => useRuleEngine(context, { rules, executeRulesSync: false })); + + // Wait for debounced execution + await vi.advanceTimersByTimeAsync(500); + + // Assert + expect(result.current).toEqual(expectedResults); + expect(executeRules).toHaveBeenCalledWith(context, rules); + }); + + it('should execute rules on initialize when runOnInitialize is true', () => { + // Arrange + const context = { foo: 'bar' }; + const rules = [{ engine: 'json-logic', value: true }]; + const expectedResults = [{ rule: rules[0], result: true }]; + + (executeRules as Mock).mockReturnValue(expectedResults); + + // Act + const { result } = renderHook(() => useRuleEngine(context, { rules, runOnInitialize: true })); + + // Assert + expect(result.current).toEqual(expectedResults); + expect(executeRules).toHaveBeenCalledWith(context, rules); + }); + + it('should convert single rule to array', () => { + // Arrange + const context = { foo: 'bar' }; + const rule = { engine: 'json-logic', value: true }; + const expectedResults = [{ rule, result: true }]; + + (executeRules as Mock).mockReturnValue(expectedResults); + + // Act + const { result } = renderHook(() => + useRuleEngine(context, { rules: rule, executeRulesSync: true }), + ); + + // Assert + expect(result.current).toEqual(expectedResults); + expect(executeRules).toHaveBeenCalledWith(context, [rule]); + }); + + it('should use custom execution delay', async () => { + // Arrange + const context = { foo: 'bar' }; + const rules = [{ engine: 'json-logic', value: true }]; + const customDelay = 1000; + const expectedResults = [{ rule: rules[0], result: true }]; + + (executeRules as Mock).mockReturnValue(expectedResults); + + // Act + const { result } = renderHook(() => + useRuleEngine(context, { rules, executeRulesSync: false, executionDelay: customDelay }), + ); + + // Assert initial empty state + expect(result.current).toEqual([]); + + // Wait for custom delayed execution + await vi.advanceTimersByTimeAsync(customDelay); + + // Assert after delay + expect(result.current).toEqual(expectedResults); + expect(executeRules).toHaveBeenCalledWith(context, rules); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/execute-rule.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/execute-rule.ts new file mode 100644 index 0000000000..6a90f7fcdd --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/execute-rule.ts @@ -0,0 +1,8 @@ +import { getRuleEngineRunner } from '../../rule-engine.repository'; +import { IRule } from '../../types'; + +export const executeRule = (context: object, rule: IRule) => { + const runEngine = getRuleEngineRunner(rule.engine); + + return runEngine(context, rule); +}; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/execute-rule.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/execute-rule.unit.test.ts new file mode 100644 index 0000000000..689c09fde2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/execute-rule.unit.test.ts @@ -0,0 +1,43 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getRuleEngineRunner } from '../../rule-engine.repository'; +import { IRule } from '../../types'; +import { executeRule } from './execute-rule'; + +vi.mock('../../rule-engine.repository', () => ({ + getRuleEngineRunner: vi.fn(), +})); + +describe('executeRule', () => { + const mockContext = { foo: 'bar' }; + const mockRule: IRule = { + engine: 'json-logic', + value: {}, + }; + const mockRunEngine = vi.fn(); + + beforeEach(() => { + vi.mocked(getRuleEngineRunner).mockReturnValue(mockRunEngine); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should get the correct rule engine runner', () => { + executeRule(mockContext, mockRule); + expect(getRuleEngineRunner).toHaveBeenCalledWith(mockRule.engine); + }); + + it('should execute the rule engine with correct parameters', () => { + executeRule(mockContext, mockRule); + expect(mockRunEngine).toHaveBeenCalledWith(mockContext, mockRule); + }); + + it('should return the result from the rule engine', () => { + const expectedResult = true; + mockRunEngine.mockReturnValue(expectedResult); + + const result = executeRule(mockContext, mockRule); + expect(result).toBe(expectedResult); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/index.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/index.ts new file mode 100644 index 0000000000..1279441981 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rule/index.ts @@ -0,0 +1 @@ +export * from './execute-rule'; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/execute-rules.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/execute-rules.ts new file mode 100644 index 0000000000..a56de8fbfb --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/execute-rules.ts @@ -0,0 +1,9 @@ +import { IRule } from '../../types'; +import { executeRule } from '../execute-rule'; + +export const executeRules = (context: object, rules: Array>) => { + return rules.map(rule => ({ + rule, + result: executeRule(context, rule as IRule), + })); +}; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/execute-rules.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/execute-rules.unit.test.ts new file mode 100644 index 0000000000..31e5d74b97 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/execute-rules.unit.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from 'vitest'; +import { executeRule } from '../execute-rule'; +import { executeRules } from './execute-rules'; + +vi.mock('../execute-rule', () => ({ + executeRule: vi.fn(), +})); + +describe('executeRules', () => { + it('should execute each rule and return array of results', () => { + // Arrange + const context = { foo: 'bar' }; + const rules = [ + { engine: 'json-logic', value: true }, + { engine: 'json-schema', value: false }, + ]; + + const mockResults = [true, false]; + mockResults.forEach((result, index) => { + (executeRule as any).mockReturnValueOnce(result); + }); + + // Act + const results = executeRules(context, rules); + + // Assert + expect(results).toEqual([ + { rule: rules[0], result: true }, + { rule: rules[1], result: false }, + ]); + + expect(executeRule).toHaveBeenCalledTimes(2); + expect(executeRule).toHaveBeenNthCalledWith(1, context, rules[0]); + expect(executeRule).toHaveBeenNthCalledWith(2, context, rules[1]); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/index.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/index.ts new file mode 100644 index 0000000000..9f1f7dbbc5 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules/index.ts @@ -0,0 +1 @@ +export * from './execute-rules'; diff --git a/packages/ui/src/components/organisms/Renderer/Renderer.tsx b/packages/ui/src/components/organisms/Renderer/Renderer.tsx index 518c9e072b..e478cb3780 100644 --- a/packages/ui/src/components/organisms/Renderer/Renderer.tsx +++ b/packages/ui/src/components/organisms/Renderer/Renderer.tsx @@ -14,9 +14,11 @@ export const Renderer: React.FunctionComponent=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 semver: 6.3.1 @@ -4944,13 +4971,13 @@ packages: semver: 6.3.1 dev: true - /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.23.7): + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.25.2): resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.22.5 regexpu-core: 5.3.2 semver: 6.3.1 @@ -4972,12 +4999,12 @@ packages: - supports-color dev: true - /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.23.7): + /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.25.2): resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 debug: 4.3.6 @@ -5067,6 +5094,20 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true + /@babel/helper-module-transforms@7.23.3(@babel/core@7.25.2): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + /@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2): resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} engines: {node: '>=6.9.0'} @@ -5105,13 +5146,13 @@ packages: '@babel/helper-wrap-function': 7.22.20 dev: true - /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.7): + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.25.2): resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-wrap-function': 7.22.20 @@ -5129,13 +5170,13 @@ packages: '@babel/helper-optimise-call-expression': 7.22.5 dev: true - /@babel/helper-replace-supers@7.22.20(@babel/core@7.23.7): + /@babel/helper-replace-supers@7.22.20(@babel/core@7.25.2): resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-member-expression-to-functions': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 @@ -5298,13 +5339,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.23.7): + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5320,25 +5361,25 @@ packages: '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.17.9) dev: true - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.23.7): + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.25.2) dev: true - /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.23.7): + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5543,13 +5584,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.7): + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2): resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 dev: true /@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.17.9): @@ -5596,6 +5637,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.2): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: @@ -5623,6 +5673,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.2): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.17.9): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} @@ -5633,13 +5692,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.7): + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.2): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5652,12 +5711,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.25.2): resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5670,12 +5729,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.25.2): resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5689,23 +5748,23 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.23.7): + /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5718,6 +5777,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.2): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: @@ -5736,6 +5804,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.2): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.17.9): resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} engines: {node: '>=6.9.0'} @@ -5784,6 +5861,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.2): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: @@ -5802,6 +5888,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.2): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.17.9): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: @@ -5820,6 +5915,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.2): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: @@ -5838,6 +5942,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.2): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: @@ -5856,6 +5969,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.2): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: @@ -5874,6 +5996,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.2): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.17.9): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} @@ -5884,13 +6015,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.7): + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.2): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5914,6 +6045,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.2): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.17.9): resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} engines: {node: '>=6.9.0'} @@ -5934,14 +6075,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.7): + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.25.2): resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5955,27 +6096,27 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-async-generator-functions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-async-generator-functions@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-59GsVNavGxAXCDDbakWSMJhajASb4kBCqDjqJsv+p5nKdbz7istmZ3HrX3L2LuiI80+zsOADCvooqQH3qGCucQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.7) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.7) + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) dev: true /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.17.9): @@ -5990,16 +6131,16 @@ packages: '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.17.9) dev: true - /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-module-imports': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.7) + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.25.2) dev: true /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.17.9): @@ -6012,13 +6153,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6032,37 +6173,37 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-block-scoping@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-block-scoping@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-QPZxHrThbQia7UdvfpaRRlq/J9ciz1J4go0k+lPBXbgaNeY7IQrBj/9ceWjvMMI07/ZBzHl/F0R/2K0qH7jCVw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-class-static-block@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-class-static-block@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-PENDVxdr7ZxKPyi5Ffc0LjXdnJyrJxyqF5T5YjlVg4a0VFfQHW0r8iAtRiDXkfHlu1wwcvdtnndGYIeJLSuRMQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) dev: true /@babel/plugin-transform-classes@7.23.3(@babel/core@7.17.9): @@ -6083,20 +6224,20 @@ packages: globals: 11.12.0 dev: true - /@babel/plugin-transform-classes@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-classes@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) '@babel/helper-split-export-declaration': 7.22.6 globals: 11.12.0 dev: true @@ -6112,13 +6253,13 @@ packages: '@babel/template': 7.22.15 dev: true - /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/template': 7.22.15 dev: true @@ -6133,13 +6274,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6154,14 +6295,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6175,25 +6316,25 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-dynamic-import@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-dynamic-import@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-vTG+cTGxPFou12Rj7ll+eD5yWeNl5/8xvQvF08y5Gv3v4mZQoyFf8/n9zg4q5vvCWt5jmgymfzMAldO7orBn7A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.17.9): @@ -6207,26 +6348,26 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-export-namespace-from@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-export-namespace-from@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-yCLhW34wpJWRdTxxWtFZASJisihrfyMOTOQexhVzA78jlU+dH7Dw+zQgcPepQ5F3C6bAIiblZZ+qBggJdHiBAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.23.7): @@ -6250,13 +6391,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-for-of@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-for-of@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6272,27 +6413,27 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-function-name': 7.23.0 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-json-strings@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-json-strings@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-H9Ej2OiISIZowZHaBwF0tsJOih1PftXJtE8EWqlEIwpc7LMTGq0rPOrywKLQ4nefzx8/HMR0D3JGXoMHYvhi0A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-literals@7.23.3(@babel/core@7.17.9): @@ -6305,25 +6446,25 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-literals@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-logical-assignment-operators@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-logical-assignment-operators@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-+pD5ZbxofyOygEp+zZAfujY2ShNCXRpDRIPOiBmTO693hhyOEteZgl876Xs9SAHPQpcV0vz8LvA/T+w8AzyX8A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) dev: true /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.17.9): @@ -6336,13 +6477,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6357,14 +6498,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6387,7 +6528,19 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.23.7 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.25.2): + resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-simple-access': 7.22.5 dev: true @@ -6405,15 +6558,15 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true - /@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-identifier': 7.22.20 dev: true @@ -6429,14 +6582,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6451,14 +6604,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.23.7): + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.25.2): resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6472,50 +6625,50 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-nullish-coalescing-operator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-nullish-coalescing-operator@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-xzg24Lnld4DYIdysyf07zJ1P+iIfJpxtVFOzX4g+bsJ3Ng5Le7rXx9KwqKzuyaUeRnt+I1EICwQITqc0E2PmpA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) dev: true - /@babel/plugin-transform-numeric-separator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-numeric-separator@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-s9GO7fIBi/BLsZ0v3Rftr6Oe4t0ctJ8h4CCXfPoEJwmvAPMyNrfkOOJzm6b9PX9YXcCJWWQd/sBF/N26eBiMVw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) dev: true - /@babel/plugin-transform-object-rest-spread@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-object-rest-spread@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-VxHt0ANkDmu8TANdE9Kc0rndo/ccsmfe2Cx2y5sI4hu3AukHQ5wAu4cM7j3ba8B9548ijVyclBU+nuDQftZsog==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/compat-data': 7.23.5 - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.17.9): @@ -6529,26 +6682,26 @@ packages: '@babel/helper-replace-supers': 7.22.20(@babel/core@7.17.9) dev: true - /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) dev: true - /@babel/plugin-transform-optional-catch-binding@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-optional-catch-binding@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-LxYSb0iLjUamfm7f1D7GpiS4j0UAC8AOiehnsGAP8BEsIX8EOi3qV6bbctw8M7ZvLtcoZfZX5Z7rN9PlWk0m5A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-optional-chaining@7.23.3(@babel/core@7.17.9): @@ -6563,16 +6716,16 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.17.9) dev: true - /@babel/plugin-transform-optional-chaining@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-optional-chaining@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-zvL8vIfIUgMccIAK1lxjvNv572JHFJIKb4MWBz5OGdBQA0fB0Xluix5rmOby48exiJc987neOmP/m9Fnpkz3Tg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.17.9): @@ -6585,38 +6738,38 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-private-property-in-object@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-private-property-in-object@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-a5m2oLNFyje2e/rGKjVfAELTVI5mbA0FeZpBnkOWWV7eSmKQ+T/XW0Vf+29ScLzSxX+rnsarvU0oie/4m6hkxA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) dev: true /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.17.9): @@ -6629,13 +6782,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6749,13 +6902,13 @@ packages: regenerator-transform: 0.15.2 dev: true - /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 regenerator-transform: 0.15.2 dev: true @@ -6770,13 +6923,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6790,13 +6943,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6811,13 +6964,13 @@ packages: '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true - /@babel/plugin-transform-spread@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-spread@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true @@ -6832,13 +6985,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6852,13 +7005,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6872,13 +7025,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6918,24 +7071,24 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6950,25 +7103,25 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -7068,80 +7221,171 @@ packages: '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.7) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.7) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.7) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.7) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.7) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.7) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.7) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.7) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.7) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.23.7) - '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-async-generator-functions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-block-scoping': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-class-static-block': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-classes': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-dynamic-import': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-export-namespace-from': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-json-strings': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-logical-assignment-operators': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.23.7) - '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-numeric-separator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-object-rest-spread': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-optional-catch-binding': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-private-property-in-object': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.23.7) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.23.7) - babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.23.7) - babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.23.7) - babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.23.7) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.2) + '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-async-generator-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoping': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-class-static-block': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-classes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-dynamic-import': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-export-namespace-from': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-json-strings': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-logical-assignment-operators': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-nullish-coalescing-operator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-numeric-separator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-object-rest-spread': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-optional-catch-binding': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-private-property-in-object': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.25.2) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.2) + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.25.2) + babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.25.2) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.25.2) + core-js-compat: 3.33.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-env@7.23.3(@babel/core@7.25.2): + resolution: {integrity: sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.23.5 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.2) + '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-async-generator-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoping': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-class-static-block': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-classes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-dynamic-import': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-export-namespace-from': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-json-strings': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-logical-assignment-operators': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-nullish-coalescing-operator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-numeric-separator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-object-rest-spread': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-optional-catch-binding': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-private-property-in-object': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.25.2) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.2) + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.25.2) + babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.25.2) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.25.2) core-js-compat: 3.33.2 semver: 6.3.1 transitivePeerDependencies: @@ -7173,12 +7417,12 @@ packages: esutils: 2.0.3 dev: true - /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.23.7): + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.25.2): resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/types': 7.23.6 esutils: 2.0.3 @@ -7443,9 +7687,9 @@ packages: eslint-plugin-prefer-arrow: ^1.2.3 eslint-plugin-unused-imports: ^3.0.0 dependencies: - '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@5.1.6) - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.1.6) - '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.1.6) + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.5.4) eslint: 8.54.0 eslint-config-prettier: 9.0.0(eslint@8.54.0) eslint-plugin-prefer-arrow: 1.2.3(eslint@8.54.0) @@ -10998,7 +11242,7 @@ packages: '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) '@mui/base': 5.0.0-beta.24(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@mui/core-downloads-tracker': 5.14.18 - '@mui/system': 5.15.6(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) '@mui/types': 7.2.13(@types/react@18.2.37) '@mui/utils': 5.15.6(@types/react@18.2.37)(react@18.2.0) '@types/react': 18.2.37 @@ -11068,27 +11312,6 @@ packages: react: 18.2.0 dev: false - /@mui/styled-engine@5.15.6(@emotion/react@11.11.1)(react@18.2.0): - resolution: {integrity: sha512-KAn8P8xP/WigFKMlEYUpU9z2o7jJnv0BG28Qu1dhNQVutsLVIFdRf5Nb+0ijp2qgtcmygQ0FtfRuXv5LYetZTg==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@emotion/react': ^11.4.1 - '@emotion/styled': ^11.3.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@emotion/react': - optional: true - '@emotion/styled': - optional: true - dependencies: - '@babel/runtime': 7.23.8 - '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) - csstype: 3.1.3 - prop-types: 15.8.1 - react: 18.2.0 - dev: false - /@mui/system@5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-J01D//u8IfXvaEHMBQX5aO2l7Q+P15nt96c4NskX7yp5/+UuZP8XCQJhtBtLuj+M2LLyXHYGmCPeblsmmscP2Q==} engines: {node: '>=12.0.0'} @@ -11139,7 +11362,7 @@ packages: '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0) '@mui/private-theming': 5.15.6(@types/react@18.2.43)(react@18.2.0) - '@mui/styled-engine': 5.15.6(@emotion/react@11.11.1)(react@18.2.0) + '@mui/styled-engine': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.13(@types/react@18.2.43) '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 @@ -11149,35 +11372,6 @@ packages: react: 18.2.0 dev: false - /@mui/system@5.15.6(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0): - resolution: {integrity: sha512-J01D//u8IfXvaEHMBQX5aO2l7Q+P15nt96c4NskX7yp5/+UuZP8XCQJhtBtLuj+M2LLyXHYGmCPeblsmmscP2Q==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@emotion/react': ^11.5.0 - '@emotion/styled': ^11.3.0 - '@types/react': ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@emotion/react': - optional: true - '@emotion/styled': - optional: true - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.23.8 - '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) - '@mui/private-theming': 5.15.6(@types/react@18.2.37)(react@18.2.0) - '@mui/styled-engine': 5.15.6(@emotion/react@11.11.1)(react@18.2.0) - '@mui/types': 7.2.13(@types/react@18.2.37) - '@mui/utils': 5.15.6(@types/react@18.2.37)(react@18.2.0) - '@types/react': 18.2.37 - clsx: 2.1.1 - csstype: 3.1.3 - prop-types: 15.8.1 - react: 18.2.0 - dev: false - /@mui/types@7.2.13(@types/react@18.2.37): resolution: {integrity: sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==} peerDependencies: @@ -11278,10 +11472,10 @@ packages: '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) '@mui/base': 5.0.0-beta.24(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@mui/material': 5.14.18(@emotion/react@11.11.1)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.15.6(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) '@mui/utils': 5.15.6(@types/react@18.2.37)(react@18.2.0) '@types/react-transition-group': 4.4.9 - clsx: 2.1.1 + clsx: 2.1.0 date-fns: 3.6.0 dayjs: 1.11.10 prop-types: 15.8.1 @@ -11392,7 +11586,7 @@ packages: '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react@18.2.0) '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) '@types/react-transition-group': 4.4.9 - clsx: 2.1.1 + clsx: 2.1.0 dayjs: 1.11.10 prop-types: 15.8.1 react: 18.2.0 @@ -15024,8 +15218,8 @@ packages: '@rjsf/utils': ^5.12.x dependencies: '@rjsf/utils': 5.14.2(react@18.2.0) - ajv: 8.12.0 - ajv-formats: 2.1.1(ajv@8.12.0) + ajv: 8.13.0 + ajv-formats: 2.1.1(ajv@8.13.0) lodash: 4.17.21 lodash-es: 4.17.21 dev: false @@ -17382,7 +17576,7 @@ packages: hasBin: true dependencies: '@babel/core': 7.23.7 - '@babel/preset-env': 7.23.3(@babel/core@7.23.7) + '@babel/preset-env': 7.23.3(@babel/core@7.25.2) '@babel/types': 7.23.6 '@ndelangen/get-tarball': 3.0.9 '@storybook/codemod': 7.5.3 @@ -22570,6 +22764,14 @@ packages: ajv: 8.12.0 dev: false + /ajv-errors@3.0.0(ajv@8.13.0): + resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} + peerDependencies: + ajv: ^8.0.1 + dependencies: + ajv: 8.13.0 + dev: false + /ajv-formats@2.1.1(ajv@8.12.0): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -23252,14 +23454,14 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.23.7): + /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.25.2): resolution: {integrity: sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: '@babel/compat-data': 7.23.5 - '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -23277,13 +23479,13 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.23.7): + /babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.25.2): resolution: {integrity: sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) core-js-compat: 3.33.2 transitivePeerDependencies: - supports-color @@ -23300,13 +23502,13 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.23.7): + /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.25.2): resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) transitivePeerDependencies: - supports-color dev: true @@ -27386,7 +27588,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.22.0)(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.22.0)(typescript@5.5.4) eslint: 8.22.0 eslint-rule-composer: 0.3.0 dev: true @@ -27416,7 +27618,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.1.6) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.5.4) eslint: 8.54.0 eslint-rule-composer: 0.3.0 dev: true From 86b9afef4e698a19c13bbd5488b9c8e9ccaec82f Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Wed, 11 Dec 2024 17:02:15 +0200 Subject: [PATCH 10/54] feat: implemented dynamic form context logic & tests & types --- .../Form/DynamicForm/DynamicForm.tsx | 51 ++++++++- .../context/dynamic-form.context.ts | 4 + .../context/hooks/useDynamicForm/index.ts | 1 + .../hooks/useDynamicForm/useDynamicForm.ts | 4 + .../useDynamicForm.unit.test.ts | 36 ++++++ .../Form/DynamicForm/context/index.ts | 3 + .../Form/DynamicForm/context/types.ts | 9 ++ .../fields/FieldList/FieldList.tsx | 31 ++--- .../hooks/useFieldList/useFieldList.ts | 21 ++-- .../StackProvider/StackProvider.unit.test.tsx | 68 +++++++++++ .../hooks/useStack/useStack.unit.test.ts | 36 ++++++ ...vert-form-emenents-to-validation-schema.ts | 20 ++++ ...emenents-to-validation-schema.unit.test.ts | 87 ++++++++++++++ .../index.ts | 1 + .../hooks/external/useElement/index.ts | 1 + .../hooks/external/useElement/useElement.ts | 5 + .../hooks/internal/useFieldHelpers/index.ts | 1 + .../hooks/internal/useFieldHelpers/types.ts | 6 + .../useFieldHelpers/useFieldHelpers.ts | 39 +++++++ .../useFieldHelpers.unit.test.ts | 98 ++++++++++++++++ .../hooks/internal/useSubmit/index.ts | 1 + .../hooks/internal/useSubmit/useSubmit.ts | 17 +++ .../internal/useSubmit/useSubmit.unit.test.ts | 58 ++++++++++ .../generate-touched-map-for-all-elements.ts | 37 ++++++ ...-touched-map-for-all-elements.unit.test.ts | 68 +++++++++++ .../index.ts | 1 + .../hooks/internal/useTouched/index.ts | 2 + .../hooks/internal/useTouched/types.ts | 3 + .../hooks/internal/useTouched/useTouched.ts | 22 ++++ .../useTouched/useTouched.unit.test.ts | 107 ++++++++++++++++++ .../internal/useValidationSchema/index.ts | 1 + .../useValidationSchema.ts | 12 ++ .../useValidationSchema.unit.test.ts | 87 ++++++++++++++ .../hooks/internal/useValues/index.ts | 1 + .../hooks/internal/useValues/useValues.ts | 40 +++++++ .../internal/useValues/useValues.unit.test.ts | 98 ++++++++++++++++ .../organisms/Form/DynamicForm/types/index.ts | 47 +++++++- .../Form/Validator/ValidatorProvider.tsx | 13 ++- .../organisms/Form/Validator/types/index.ts | 7 +- .../components/organisms/Renderer/types.ts | 2 - packages/ui/tsconfig.json | 2 +- 41 files changed, 1104 insertions(+), 44 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/context/dynamic-form.context.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/context/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/types.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/types.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/useValues.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/useValues.unit.test.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx index b67908a0fe..a7a9b0d457 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -1,3 +1,50 @@ -export const DynamicForm = () => { - return
DynamicForm
; +import { FunctionComponent, useMemo } from 'react'; + +import { Renderer, TRendererSchema } from '../../Renderer'; +import { ValidatorProvider } from '../Validator'; +import { DynamicFormContext, IDynamicFormContext } from './context'; +import { useFieldHelpers } from './hooks/internal/useFieldHelpers'; +import { useSubmit } from './hooks/internal/useSubmit'; +import { useTouched } from './hooks/internal/useTouched'; +import { useValidationSchema } from './hooks/internal/useValidationSchema'; +import { useValues } from './hooks/internal/useValues'; +import { IDynamicFormProps } from './types'; + +export const DynamicForm: FunctionComponent = ({ + elements, + values: initialValues, + validationParams, + elementsMap, + onChange, + onFieldChange, + onSubmit, +}) => { + const validationSchema = useValidationSchema(elements); + const valuesApi = useValues({ + values: initialValues, + onChange, + onFieldChange, + }); + const touchedApi = useTouched(elements, valuesApi.values); + const fieldHelpers = useFieldHelpers({ valuesApi, touchedApi }); + const { submit } = useSubmit({ values: valuesApi.values, onSubmit }); + + const context: IDynamicFormContext = useMemo( + () => ({ + touched: touchedApi.touched, + values: valuesApi.values, + submit, + fieldHelpers, + elementsMap, + }), + [touchedApi.touched, valuesApi.values, submit, fieldHelpers, elementsMap], + ); + + return ( + + + + + + ); }; 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 new file mode 100644 index 0000000000..f5617e6457 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/dynamic-form.context.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import { IDynamicFormContext } from './types'; + +export const DynamicFormContext = createContext({} as IDynamicFormContext); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/index.ts new file mode 100644 index 0000000000..24344cc5a4 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/index.ts @@ -0,0 +1 @@ +export * from './useDynamicForm'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.ts b/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.ts new file mode 100644 index 0000000000..3841cae262 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { DynamicFormContext } from '../../dynamic-form.context'; + +export const useDynamicForm = () => useContext(DynamicFormContext); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts new file mode 100644 index 0000000000..00d99c03cb --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts @@ -0,0 +1,36 @@ +import { renderHook } from '@testing-library/react'; +import { useContext } from 'react'; +import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { DynamicFormContext } from '../../dynamic-form.context'; +import { useDynamicForm } from './useDynamicForm'; + +vi.mock('react', () => ({ + useContext: vi.fn(), + createContext: vi.fn(), +})); + +describe('useDynamicForm', () => { + const mockContextValue = { + values: { field1: 'value1' }, + touched: { field1: true }, + submit: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + (useContext as Mock).mockReturnValue(mockContextValue); + }); + + it('should call useContext with DynamicFormContext', () => { + renderHook(() => useDynamicForm()); + + expect(useContext).toHaveBeenCalledTimes(1); + expect(useContext).toHaveBeenCalledWith(DynamicFormContext); + }); + + it('should return context value', () => { + const { result } = renderHook(() => useDynamicForm()); + + expect(result.current).toBe(mockContextValue); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/context/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/context/index.ts new file mode 100644 index 0000000000..c6faf6c81f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/index.ts @@ -0,0 +1,3 @@ +export * from './dynamic-form.context'; +export * from './hooks/useDynamicForm'; +export * from './types'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts b/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts new file mode 100644 index 0000000000..020d44161a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts @@ -0,0 +1,9 @@ +import { ITouchedState } from '../hooks/internal/useTouched'; +import { TElementsMap } from '../types'; + +export interface IDynamicFormContext { + values: TValues; + touched: ITouchedState; + elementsMap: TElementsMap; + submit: () => void; +} diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx index 4e37a4ca88..d2629c33c0 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx @@ -1,12 +1,10 @@ -import { useFieldList } from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/hooks/useFieldList'; -import { - StackProvider, - useStack, -} from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider'; -import { rendererSchema } from '@/pages/CollectionFlowV2/renderer-schema'; -import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; -import { AnyObject, Button, Renderer } from '@ballerine/ui'; -import { FunctionComponent } from 'react'; +import { AnyObject } from '@/common'; +import { Button } from '@/components/atoms'; +import { Renderer, TRendererSchema } from '@/components/organisms/Renderer'; +import { useDynamicForm } from '../../context'; +import { TBaseFormElements, TDynamicFormElement } from '../../types'; +import { useFieldList } from './hooks/useFieldList'; +import { StackProvider, useStack } from './providers/StackProvider'; export type TFieldListValueType = T[]; @@ -16,12 +14,14 @@ export interface IFieldListOptions { removeButtonLabel?: string; } -export const FieldList: FunctionComponent< - IFieldComponentProps, IFieldListOptions> +export const FieldList: TDynamicFormElement< + TBaseFormElements, + { addButtonLabel: string; removeButtonLabel: string } > = props => { + const { elementsMap } = useDynamicForm(); const { stack } = useStack(); - const { definition, options } = props; - const { addButtonLabel = 'Add Item', removeButtonLabel = 'Remove' } = options || {}; + const { element } = props; + const { addButtonLabel = 'Add Item', removeButtonLabel = 'Remove' } = element.params || {}; const { items, addItem, removeItem } = useFieldList(props); return ( @@ -35,7 +35,10 @@ export const FieldList: FunctionComponent< - + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts index b467719a58..21e8e2599a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts @@ -1,24 +1,17 @@ -import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; -import { - IFieldListOptions, - TFieldListValueType, -} from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/FieldList'; import { useUIElement } from '@/pages/CollectionFlowV2/hooks/useUIElement'; -import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; import set from 'lodash/set'; import { useCallback, useMemo } from 'react'; import { v4 as uuidv4 } from 'uuid'; +import { IFormElement } from '../../../../types'; -export const useFieldList = ({ - definition, - stack, - fieldProps, - options, -}: IFieldComponentProps, IFieldListOptions>) => { - const { payload, stateApi } = useStateManagerContext(); +export interface IUseFieldListProps { + element: IFormElement; +} + +export const useFieldList = ({}) => { const uiElement = useUIElement(definition, payload, stack); - const items = useMemo(() => (uiElement.getValue() as { _id: string }[]) || [], [uiElement]); + const items = useMemo(() => (uiElement.getValue() as Array<{ _id: string }>) || [], [uiElement]); const addItem = useCallback(() => { const valueDestination = uiElement.getValueDestination(); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.unit.test.tsx new file mode 100644 index 0000000000..450f2f2cb3 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.unit.test.tsx @@ -0,0 +1,68 @@ +vi.mock('react', () => ({ + useMemo: vi.fn(vi.fn()), + createContext: vi.fn(() => ({ + Provider: vi.fn(), + })), +})); + +vi.mock('./context/stack-provider-context', () => ({ + StackProviderContext: { + Provider: vi.fn(), + }, +})); + +import { render } from '@testing-library/react'; +import { useMemo } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { StackProvider } from './StackProvider'; +import { StackProviderContext } from './context/stack-provider-context'; + +describe('StackProvider', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('should create context with provided stack', () => { + const mockStack = [1, 2, 3]; + + render( + +
Test Child
+
, + ); + + expect(useMemo).toHaveBeenCalled(); + expect(vi.mocked(useMemo).mock.calls[0]?.[0]()).toEqual({ stack: mockStack }); + }); + + it('should create context with empty array stack when not provided', () => { + render( + +
Test Child
+
, + ); + + expect(useMemo).toHaveBeenCalled(); + expect(vi.mocked(useMemo).mock.calls[0]?.[0]()).toEqual({ stack: undefined }); + }); + + it('should pass context value to provider', () => { + const mockStack = [1, 2, 3]; + const mockContext = { stack: mockStack }; + + vi.mocked(useMemo).mockReturnValue(mockContext); + + render( + +
Test Child
+
, + ); + + expect(StackProviderContext.Provider).toHaveBeenCalledWith( + expect.objectContaining({ + value: mockContext, + children: expect.anything(), + }), + {}, + ); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.unit.test.ts new file mode 100644 index 0000000000..dac8176782 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.unit.test.ts @@ -0,0 +1,36 @@ +import { renderHook } from '@testing-library/react'; +import { useContext } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { StackProviderContext } from '../../context/stack-provider-context'; +import { useStack } from './useStack'; + +vi.mock('react', () => ({ + useContext: vi.fn(), + createContext: vi.fn(), +})); + +describe('useStack', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return context from StackProviderContext', () => { + const mockContextValue = { stack: [1, 2, 3] }; + vi.mocked(useContext).mockReturnValue(mockContextValue); + + const { result } = renderHook(() => useStack()); + + expect(useContext).toHaveBeenCalledWith(StackProviderContext); + expect(result.current).toBe(mockContextValue); + }); + + it('should return empty stack when context is empty', () => { + const mockContextValue = { stack: [] }; + vi.mocked(useContext).mockReturnValue(mockContextValue); + + const { result } = renderHook(() => useStack()); + + expect(useContext).toHaveBeenCalledWith(StackProviderContext); + expect(result.current).toBe(mockContextValue); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.ts new file mode 100644 index 0000000000..f4b983bb92 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.ts @@ -0,0 +1,20 @@ +import { IValidationSchema } from '../../../Validator'; +import { IFormElement } from '../../types'; + +export const convertFormElementsToValidationSchema = ( + elements: Array>, +): IValidationSchema[] => { + return elements.map(element => { + const validationSchema: IValidationSchema = { + id: element.id, + valueDestination: element.valueDestination, + validators: element.validate || [], + }; + + if (element.children) { + validationSchema.children = convertFormElementsToValidationSchema(element.children); + } + + return validationSchema; + }); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.unit.test.ts new file mode 100644 index 0000000000..29766099df --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.unit.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, test } from 'vitest'; +import { IValidationSchema } from '../../../Validator'; +import { IFormElement } from '../../types'; +import { convertFormElementsToValidationSchema } from './convert-form-emenents-to-validation-schema'; + +describe('convertFormElementsToValidationSchema', () => { + const case1 = [ + [{ id: '1', valueDestination: 'test', validate: [], element: 'textinput' }] as IFormElement[], + [ + { id: '1', valueDestination: 'test', validators: [], children: undefined }, + ] as IValidationSchema[], + ] as const; + + const case2 = [ + [ + { + id: 'fieldlist', + valueDestination: 'test', + validate: [ + { + type: 'required', + }, + ], + element: 'fieldlist', + children: [ + { + id: 'textinput', + valueDestination: 'test', + element: 'textinput', + validate: [ + { + type: 'required', + }, + ], + }, + { + id: 'nested-fieldlist', + valueDestination: 'test', + element: 'fieldlist', + validate: [{ type: 'required' }], + children: [ + { + id: 'nested-textinput', + valueDestination: 'test', + element: 'textinput', + validate: [ + { + type: 'required', + }, + ], + }, + ], + }, + ], + }, + ] as IFormElement[], + [ + { + id: 'fieldlist', + valueDestination: 'test', + validators: [{ type: 'required' }], + children: [ + { id: 'textinput', valueDestination: 'test', validators: [{ type: 'required' }] }, + { + id: 'nested-fieldlist', + valueDestination: 'test', + validators: [{ type: 'required' }], + children: [ + { + id: 'nested-textinput', + valueDestination: 'test', + validators: [{ type: 'required' }], + }, + ], + }, + ], + }, + ], + ] as const; + + const cases = [case1, case2]; + + test.each(cases)('should convert form elements to validation schema', (schema, output) => { + const validationSchema = convertFormElementsToValidationSchema(schema); + expect(validationSchema).toEqual(output); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/index.ts new file mode 100644 index 0000000000..49cfd6bbb5 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/index.ts @@ -0,0 +1 @@ +export * from './convert-form-emenents-to-validation-schema'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/index.ts new file mode 100644 index 0000000000..0ac831e622 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/index.ts @@ -0,0 +1 @@ +export * from './useElement'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts new file mode 100644 index 0000000000..a16b586450 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts @@ -0,0 +1,5 @@ +import { IFormElement } from '../../../types'; + +export const useElement = (element: IFormElement) => { + return {}; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/index.ts new file mode 100644 index 0000000000..43e477580e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/index.ts @@ -0,0 +1 @@ +export * from './useFieldHelpers'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/types.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/types.ts new file mode 100644 index 0000000000..637c07cda8 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/types.ts @@ -0,0 +1,6 @@ +export interface IFieldHelpers { + getTouched: (fieldId: string) => boolean; + getValue: (fieldId: string) => T; + setTouched: (fieldId: string, touched: boolean) => void; + setValue: (fieldId: string, value: T) => void; +} diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.ts new file mode 100644 index 0000000000..860ca8b6fe --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.ts @@ -0,0 +1,39 @@ +import { useCallback, useMemo } from 'react'; +import { useTouched } from '../useTouched'; +import { useValues } from '../useValues'; + +export interface IUseFieldHelpersParams { + valuesApi: ReturnType; + touchedApi: ReturnType; +} + +export const useFieldHelpers = ({ valuesApi, touchedApi }: IUseFieldHelpersParams) => { + const { values, setFieldValue } = valuesApi; + const { touched, setFieldTouched } = touchedApi; + + const getTouched = useCallback( + (fieldId: string) => { + return Boolean(touched[fieldId]); + }, + [touched], + ); + + const getValue = useCallback( + (fieldId: string) => { + return values[fieldId as keyof typeof values] as T; + }, + [values], + ); + + const helpers = useMemo( + () => ({ + getTouched, + getValue, + setTouched: setFieldTouched, + setValue: setFieldValue, + }), + [getTouched, getValue, setFieldTouched, setFieldValue], + ); + + return helpers; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.unit.test.ts new file mode 100644 index 0000000000..d0906fa6ff --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.unit.test.ts @@ -0,0 +1,98 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useTouched } from '../useTouched'; +import { useValues } from '../useValues'; +import { useFieldHelpers } from './useFieldHelpers'; + +vi.mock('../useTouched', () => ({ + useTouched: vi.fn(), +})); + +vi.mock('../useValues', () => ({ + useValues: vi.fn(), +})); + +describe('useFieldHelpers', () => { + const mockSetFieldValue = vi.fn(); + const mockSetFieldTouched = vi.fn(); + + const mockValuesApi = { + values: { + field1: 'value1', + field2: 'value2', + }, + setFieldValue: mockSetFieldValue, + }; + + const mockTouchedApi = { + touched: { + field1: true, + field2: false, + }, + setFieldTouched: mockSetFieldTouched, + }; + + const setup = () => { + return renderHook(() => + useFieldHelpers({ + valuesApi: mockValuesApi as unknown as ReturnType, + touchedApi: mockTouchedApi as unknown as ReturnType, + }), + ); + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return helper functions', () => { + const { result } = setup(); + + expect(result.current).toHaveProperty('getTouched'); + expect(result.current).toHaveProperty('getValue'); + expect(result.current).toHaveProperty('setTouched'); + expect(result.current).toHaveProperty('setValue'); + }); + + it('getTouched should return correct touched state', () => { + const { result } = setup(); + + expect(result.current.getTouched('field1')).toBe(true); + expect(result.current.getTouched('field2')).toBe(false); + }); + + it('getValue should return correct value', () => { + const { result } = setup(); + + expect(result.current.getValue('field1')).toBe('value1'); + expect(result.current.getValue('field2')).toBe('value2'); + }); + + it('setTouched should call touchedApi.setFieldTouched', () => { + const { result } = setup(); + + result.current.setTouched('field1', true); + + expect(mockSetFieldTouched).toHaveBeenCalledTimes(1); + expect(mockSetFieldTouched).toHaveBeenCalledWith('field1', true); + }); + + it('setValue should call valuesApi.setFieldValue', () => { + const { result } = setup(); + + result.current.setValue('field1', 'field1', 'newValue'); + + expect(mockSetFieldValue).toHaveBeenCalledTimes(1); + expect(mockSetFieldValue).toHaveBeenCalledWith('field1', 'field1', 'newValue'); + }); + + it('should memoize helper functions', () => { + const { result, rerender } = setup(); + + const firstHelpers = result.current; + + rerender(); + + expect(result.current).toBe(firstHelpers); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/index.ts new file mode 100644 index 0000000000..ec3219bb8a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/index.ts @@ -0,0 +1 @@ +export * from './useSubmit'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.ts new file mode 100644 index 0000000000..907f2cc2b2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.ts @@ -0,0 +1,17 @@ +import { useCallback } from 'react'; + +export interface IUseSubmitParams { + onSubmit?: (values: TValues) => void; + values: TValues; +} + +export const useSubmit = ({ + onSubmit, + values, +}: IUseSubmitParams) => { + const submit = useCallback(() => { + onSubmit?.(values); + }, [onSubmit, values]); + + return { submit }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.unit.test.ts new file mode 100644 index 0000000000..ad3f5b5cda --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.unit.test.ts @@ -0,0 +1,58 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useSubmit } from './useSubmit'; + +describe('useSubmit', () => { + const mockValues = { + field1: 'value1', + field2: 'value2', + }; + + const mockOnSubmit = vi.fn(); + + const setup = (params = {}) => { + return renderHook(() => + useSubmit({ + values: mockValues, + onSubmit: mockOnSubmit, + ...params, + }), + ); + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return submit function', () => { + const { result } = setup(); + + expect(result.current).toHaveProperty('submit'); + expect(typeof result.current.submit).toBe('function'); + }); + + it('should call onSubmit with values when submit is called', () => { + const { result } = setup(); + + result.current.submit(); + + expect(mockOnSubmit).toHaveBeenCalledTimes(1); + expect(mockOnSubmit).toHaveBeenCalledWith(mockValues); + }); + + it('should not throw when onSubmit is not provided', () => { + const { result } = setup({ onSubmit: undefined }); + + expect(() => result.current.submit()).not.toThrow(); + }); + + it('should memoize submit function', () => { + const { result, rerender } = setup(); + + const firstSubmit = result.current.submit; + + rerender(); + + expect(result.current.submit).toBe(firstSubmit); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements.ts new file mode 100644 index 0000000000..2410fc875e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements.ts @@ -0,0 +1,37 @@ +import { formatId } from '@/components/organisms/Form/Validator/utils/format-id'; +import { formatValueDestination } from '@/components/organisms/Form/Validator/utils/format-value-destination'; +import get from 'lodash/get'; +import { TDeepthLevelStack } from '../../../../../../Validator/types'; +import { IFormElement } from '../../../../../types'; +import { ITouchedState } from '../../types'; + +export const generateTouchedMapForAllElements = ( + elements: IFormElement[], + context: object, +): ITouchedState => { + const touchedMap: ITouchedState = {}; + + const run = (elements: IFormElement[], stack: TDeepthLevelStack = []) => { + elements.forEach(element => { + const { children, valueDestination, id } = element; + const formattedId = formatId(id, stack); + const formattedValueDestination = valueDestination + ? formatValueDestination(valueDestination, stack) + : ''; + + touchedMap[formattedId] = true; + + const value = formattedValueDestination ? get(context, formattedValueDestination) : null; + + if (children && formattedValueDestination && Array.isArray(value)) { + value.forEach((_, index) => { + run(children, [...stack, index]); + }); + } + }); + }; + + run(elements); + + return touchedMap; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements.unit.test.ts new file mode 100644 index 0000000000..2bea208344 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements.unit.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import { IFormElement } from '../../../../../types'; +import { generateTouchedMapForAllElements } from './generate-touched-map-for-all-elements'; + +describe('generateTouchedMapForAllElements', () => { + it('should generate touched map for all elements', () => { + const elements = [ + { id: '1', valueDestination: '1', children: [], validate: [], element: 'textinput' }, + { id: '2', valueDestination: '2', children: [], validate: [], element: 'textinput' }, + ] as IFormElement[]; + + expect(generateTouchedMapForAllElements(elements, {})).toEqual({ + '1': true, + '2': true, + }); + }); + + it('should generate touched map for all elements with children', () => { + const elements = [ + { + id: 'list', + valueDestination: 'list', + validate: [], + element: 'fieldlist', + children: [ + { + id: 'firstName', + valueDestination: 'list[$0].firstName', + validate: [], + element: 'textfield', + }, + { + id: 'lastName', + valueDestination: 'list[$0].lastName', + validate: [], + element: 'textfield', + }, + { + id: 'innerList', + valueDestination: 'list[$0].innerList', + validate: [], + element: 'fieldlist', + children: [ + { + id: 'innerValue', + valueDestination: 'list[$0].innerList[$1].innerValue', + validate: [], + element: 'textfield', + }, + ], + }, + ], + }, + ] as unknown as IFormElement[]; + + const context = { + list: [{ firstName: 'John', lastName: 'Doe', innerList: [{ innerValue: 'Inner' }] }], + }; + + expect(generateTouchedMapForAllElements(elements, context)).toEqual({ + list: true, + 'firstName-0': true, + 'lastName-0': true, + 'innerList-0': true, + 'innerValue-0-0': true, + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/index.ts new file mode 100644 index 0000000000..68b0b471d2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/helpers/generate-touched-map-for-all-elements/index.ts @@ -0,0 +1 @@ +export * from './generate-touched-map-for-all-elements'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/index.ts new file mode 100644 index 0000000000..da26a29ad9 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './useTouched'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/types.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/types.ts new file mode 100644 index 0000000000..552705ba58 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/types.ts @@ -0,0 +1,3 @@ +export interface ITouchedState { + [key: string]: boolean; +} diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts new file mode 100644 index 0000000000..5cde67b769 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts @@ -0,0 +1,22 @@ +import { useCallback, useState } from 'react'; +import { IFormElement } from '../../../types'; +import { generateTouchedMapForAllElements } from './helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements'; +import { ITouchedState } from './types'; + +export const useTouched = (elements: IFormElement[], context: object) => { + const [touched, setTouchedState] = useState({}); + + const setFieldTouched = useCallback((fieldName: string, isTouched: boolean) => { + setTouchedState(prev => ({ ...prev, [fieldName]: isTouched })); + }, []); + + const setTouched = useCallback((newTouched: ITouchedState) => { + setTouchedState(newTouched); + }, []); + + const touchAllFields = useCallback(() => { + setTouchedState(generateTouchedMapForAllElements(elements, context)); + }, [elements, context]); + + return { touched, setTouched, setFieldTouched, touchAllFields }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts new file mode 100644 index 0000000000..106dfa589c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts @@ -0,0 +1,107 @@ +import { act, renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { IFormElement } from '../../../types'; +import { generateTouchedMapForAllElements } from './helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements'; +import { useTouched } from './useTouched'; + +vi.mock( + './helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements', + () => ({ + generateTouchedMapForAllElements: vi.fn(), + }), +); + +describe('useTouched', () => { + const elements: IFormElement[] = [ + { id: '1', valueDestination: '1', children: [], validate: [], element: 'textinput' }, + { id: '2', valueDestination: '2', children: [], validate: [], element: 'textinput' }, + ]; + const context = {}; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should initialize with empty touched state', () => { + const { result } = renderHook(() => useTouched(elements, context)); + + expect(result.current.touched).toEqual({}); + }); + + it('should set field touched state', () => { + const { result } = renderHook(() => useTouched(elements, context)); + + act(() => { + result.current.setFieldTouched('field1', true); + }); + + expect(result.current.touched).toEqual({ field1: true }); + + act(() => { + result.current.setFieldTouched('field2', false); + }); + + expect(result.current.touched).toEqual({ field1: true, field2: false }); + }); + + it('should set touched state', () => { + const { result } = renderHook(() => useTouched(elements, context)); + const newTouchedState = { field1: true, field2: false }; + + act(() => { + result.current.setTouched(newTouchedState); + }); + + expect(result.current.touched).toEqual(newTouchedState); + }); + + it('should touch all fields', () => { + const mockTouchedMap = { '1': true, '2': true }; + (generateTouchedMapForAllElements as Mock).mockReturnValue(mockTouchedMap); + + const { result } = renderHook(() => useTouched(elements, context)); + + act(() => { + result.current.touchAllFields(); + }); + + expect(generateTouchedMapForAllElements).toHaveBeenCalledWith(elements, context); + expect(result.current.touched).toEqual(mockTouchedMap); + }); + + it('should update touched state when elements or context change', () => { + const mockTouchedMap1 = { '1': true }; + const mockTouchedMap2 = { '1': true, '2': true }; + + (generateTouchedMapForAllElements as Mock) + .mockReturnValueOnce(mockTouchedMap1) + .mockReturnValueOnce(mockTouchedMap2); + + const { result, rerender } = renderHook( + ({ elements, context }) => useTouched(elements, context), + { + initialProps: { elements, context }, + }, + ); + + act(() => { + result.current.touchAllFields(); + }); + + expect(result.current.touched).toEqual(mockTouchedMap1); + + const newElements: IFormElement[] = [ + ...elements, + { id: '3', valueDestination: '3', children: [], validate: [], element: 'textinput' }, + ]; + + rerender({ elements: newElements, context }); + + act(() => { + result.current.touchAllFields(); + }); + + expect(generateTouchedMapForAllElements).toHaveBeenCalledTimes(2); + expect(result.current.touched).toEqual(mockTouchedMap2); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/index.ts new file mode 100644 index 0000000000..ca722e2568 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/index.ts @@ -0,0 +1 @@ +export * from './useValidationSchema'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts new file mode 100644 index 0000000000..2d71dd4196 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts @@ -0,0 +1,12 @@ +import { useMemo } from 'react'; + +import { convertFormElementsToValidationSchema } from '../../../helpers/convert-form-emenents-to-validation-schema'; +import { IFormElement } from '../../../types'; + +export const useValidationSchema = (elements: IFormElement[]) => { + const validationSchema = useMemo(() => { + return convertFormElementsToValidationSchema(elements); + }, [elements]); + + return validationSchema; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.unit.test.ts new file mode 100644 index 0000000000..143a73440f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.unit.test.ts @@ -0,0 +1,87 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, Mock, test, vi } from 'vitest'; +import { convertFormElementsToValidationSchema } from '../../../helpers/convert-form-emenents-to-validation-schema'; +import { IFormElement } from '../../../types'; +import { useValidationSchema } from './useValidationSchema'; + +vi.mock('../../helpers/convert-form-emenents-to-validation-schema', () => ({ + convertFormElementsToValidationSchema: vi.fn(), +})); + +describe('useValidationSchema', () => { + const mockElements: IFormElement[] = [ + { + id: '1', + valueDestination: 'test', + element: 'textinput', + validate: [], + }, + ]; + + const mockValidationSchema = [ + { + id: '1', + valueDestination: 'test', + validators: [], + }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + (convertFormElementsToValidationSchema as Mock).mockReturnValue(mockValidationSchema); + }); + + test('should return validation schema', () => { + const { result } = renderHook(() => useValidationSchema(mockElements)); + + expect(convertFormElementsToValidationSchema).toHaveBeenCalledWith(mockElements); + expect(result.current).toEqual(mockValidationSchema); + }); + + test('should memoize validation schema', () => { + const { result, rerender } = renderHook(props => useValidationSchema(props), { + initialProps: mockElements, + }); + + const firstResult = result.current; + + // Rerender with same props + rerender(mockElements); + expect(result.current).toBe(firstResult); + expect(convertFormElementsToValidationSchema).toHaveBeenCalledTimes(1); + }); + + test('should recalculate when elements change', () => { + const { result, rerender } = renderHook(props => useValidationSchema(props), { + initialProps: mockElements, + }); + + const firstResult = result.current; + + const newElements = [ + { + id: '2', + valueDestination: 'test2', + element: 'textinput', + validate: [], + }, + ] as IFormElement[]; + + const newValidationSchema = [ + { + id: '2', + valueDestination: 'test2', + validators: [], + }, + ]; + + (convertFormElementsToValidationSchema as Mock).mockReturnValue(newValidationSchema); + + // Rerender with different props + rerender(newElements); + + expect(result.current).not.toBe(firstResult); + expect(result.current).toEqual(newValidationSchema); + expect(convertFormElementsToValidationSchema).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/index.ts new file mode 100644 index 0000000000..3fc0f89bcc --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/index.ts @@ -0,0 +1 @@ +export * from './useValues'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/useValues.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/useValues.ts new file mode 100644 index 0000000000..87723a1f8a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/useValues.ts @@ -0,0 +1,40 @@ +import set from 'lodash/set'; +import { useCallback, useState } from 'react'; + +export interface IUseValuesProps { + values: TValues; + onChange?: (newValues: TValues) => void; + onFieldChange?: (fieldName: string, newValue: unknown, newValues: TValues) => void; +} + +export const useValues = ({ + values: initialValues, + onChange, + onFieldChange, +}: IUseValuesProps) => { + const [values, setValuesState] = useState(initialValues); + + const setValues = useCallback( + (newValues: TValues) => { + setValuesState(newValues); + onChange?.(newValues); + }, + [onChange], + ); + + const setFieldValue = useCallback( + (fieldName: string, valueDestination: string, newValue: unknown) => { + setValuesState(prev => { + const newValues = { ...prev }; + set(newValues, valueDestination, newValue); + + onFieldChange?.(fieldName, newValue, newValues); + + return newValues; + }); + }, + [onFieldChange], + ); + + return { values, setValues, setFieldValue }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/useValues.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/useValues.unit.test.ts new file mode 100644 index 0000000000..21b4587cf7 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValues/useValues.unit.test.ts @@ -0,0 +1,98 @@ +import { act, renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { useValues } from './useValues'; + +describe('useValues', () => { + const initialValues = { + name: 'John', + address: { + street: 'Main St', + }, + }; + + it('should initialize with provided values', () => { + const { result } = renderHook(() => useValues({ values: initialValues })); + + expect(result.current.values).toEqual(initialValues); + }); + + it('should update values and call onChange when setValues is called', () => { + const onChange = vi.fn(); + const { result } = renderHook(() => + useValues({ + values: initialValues, + onChange, + }), + ); + + const newValues = { name: 'Jane', address: { street: 'Second St' } }; + + act(() => { + result.current.setValues(newValues); + }); + + expect(result.current.values).toEqual(newValues); + expect(onChange).toHaveBeenCalledWith(newValues); + }); + + it('should update field value and call onFieldChange when setFieldValue is called', () => { + const onFieldChange = vi.fn(); + const { result } = renderHook(() => + useValues({ + values: initialValues, + onFieldChange, + }), + ); + + act(() => { + result.current.setFieldValue('name', 'name', 'Jane'); + }); + + const expectedValues = { + ...initialValues, + name: 'Jane', + }; + + expect(result.current.values).toEqual(expectedValues); + expect(onFieldChange).toHaveBeenCalledWith('name', 'Jane', expectedValues); + }); + + it('should update nested field value correctly', () => { + const { result } = renderHook(() => + useValues({ + values: initialValues, + }), + ); + + act(() => { + result.current.setFieldValue('street', 'address.street', 'Second St'); + }); + + expect(result.current.values).toEqual({ + ...initialValues, + address: { + street: 'Second St', + }, + }); + }); + + it('should work without optional callbacks', () => { + const { result } = renderHook(() => + useValues({ + values: initialValues, + }), + ); + + act(() => { + result.current.setValues({ name: 'Jane', address: { street: 'Second St' } }); + }); + + expect(result.current.values).toEqual({ name: 'Jane', address: { street: 'Second St' } }); + + act(() => { + result.current.setFieldValue('name', 'name', 'John'); + }); + + expect(result.current.values).toEqual({ name: 'John', address: { street: 'Second St' } }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts index 608f922429..d00daaf6c4 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts @@ -1,13 +1,50 @@ -import { IValidationSchema } from '../../Validator'; +import { FunctionComponent } from 'react'; +import { IRule } from '../../hooks/useRuleEngine'; +import { IValidationError, IValidationParams, TValidators } from '../../Validator'; -export type TBaseFormElements = 'textinput'; +export type TBaseFormElements = 'textinput' | 'fieldlist'; -export interface IFormElement { +export interface IFormElement { + id: string; valueDestination: string; element: TElements; - validate?: IValidationSchema[]; + validate?: TValidators; + disable?: IRule[]; + hidden?: IRule[]; + children?: IFormElement[]; + params?: TParams; } -export interface IDynamicFormProps { +export interface IFormRef { + submit: () => void; + validate(): IValidationError[] | null; + setValues: (values: TValues) => void; + setTouched: (touched: Record) => void; + setFieldValue: (fieldName: string, value: unknown) => void; + setFieldTouched: (fieldName: string, isTouched: boolean) => void; +} + +export type TDynamicFormElement< + TElements extends string = TBaseFormElements, + TParams = unknown, +> = FunctionComponent<{ + element: IFormElement; +}>; + +export type TElementsMap = Record< + TElements, + TDynamicFormElement +>; + +export interface IDynamicFormProps { values: TValues; + elements: Array>; + elementsMap: TElementsMap; + + validationParams?: IValidationParams; + onChange?: (newValues: TValues) => void; + onFieldChange?: (fieldName: string, newValue: unknown, newValues: TValues) => void; + onSubmit?: (values: TValues) => void; + + ref?: React.RefObject>; } diff --git a/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx b/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx index db31c294ed..4ea014ac0d 100644 --- a/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx +++ b/packages/ui/src/components/organisms/Form/Validator/ValidatorProvider.tsx @@ -5,16 +5,19 @@ import { useValidate } from './hooks/internal/useValidate'; import { IValidatorRef, useValidatorRef } from './hooks/internal/useValidatorRef'; import { IValidationSchema } from './types'; -export interface IValidatorProviderProps { +export interface IValidationParams { + validateOnChange?: boolean; + validateSync?: boolean; + validationDelay?: number; + abortEarly?: boolean; +} + +export interface IValidatorProviderProps extends IValidationParams { children: React.ReactNode | React.ReactNode[]; schema: IValidationSchema[]; value: TValue; ref?: React.RefObject; - validateOnChange?: boolean; - validateSync?: boolean; - validationDelay?: number; - abortEarly?: boolean; } export const ValidatorProvider = ({ diff --git a/packages/ui/src/components/organisms/Form/Validator/types/index.ts b/packages/ui/src/components/organisms/Form/Validator/types/index.ts index 9de2abff53..a5d019c50b 100644 --- a/packages/ui/src/components/organisms/Form/Validator/types/index.ts +++ b/packages/ui/src/components/organisms/Form/Validator/types/index.ts @@ -20,13 +20,18 @@ export interface ICommonValidator = Array>; + export interface IValidationSchema< TValidatorTypeExtends extends string = TBaseValidators, TValue = object, > { id: string; valueDestination?: string; - validators: Array>; + validators: TValidators; children?: IValidationSchema[]; } diff --git a/packages/ui/src/components/organisms/Renderer/types.ts b/packages/ui/src/components/organisms/Renderer/types.ts index ad3ed97efc..e3f7656a53 100644 --- a/packages/ui/src/components/organisms/Renderer/types.ts +++ b/packages/ui/src/components/organisms/Renderer/types.ts @@ -8,11 +8,9 @@ export interface IRendererElement { export type IRendererComponent< TDefinition extends IRendererElement, TProps extends Record, - TOptions extends Record = Record, TBaseProps = { stack?: number[]; children?: React.ReactNode | React.ReactNode[]; - options?: TOptions; definition: TDefinition; }, > = React.FunctionComponent; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index c55b57d833..d41e73e2e1 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -10,5 +10,5 @@ "module": "ESNext", "moduleResolution": "bundler" }, - "include": ["src"] + "include": ["src", "vitest.setup.ts"] } From d8d4b25ed146f800549bdaad886478f6bfb7fdff Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 13 Dec 2024 14:30:15 +0200 Subject: [PATCH 11/54] feat: finalized core form logic & finalized field list & tests --- .../Form/DynamicForm/DynamicForm.tsx | 2 +- .../useDynamicForm.unit.test.ts | 4 +- .../Form/DynamicForm/context/types.ts | 2 + .../fields/FieldList/FieldList.tsx | 24 ++- .../fields/FieldList/FieldList.unit.test.tsx | 127 ++++++++++++ .../hooks/useFieldList/useFieldList.ts | 42 ++-- .../useFieldList/useFieldList.unit.test.ts | 83 ++++++++ .../Form/DynamicForm/hooks/external/index.ts | 5 + .../hooks/external/useElement/useElement.ts | 26 ++- .../useElement/useElement.unit.test.ts | 189 ++++++++++++++++++ .../hooks/external/useElementId/index.ts | 1 + .../external/useElementId/useElementId.ts | 10 + .../useElementId/useElementId.unit.test.ts | 36 ++++ .../hooks/external/useField/index.ts | 1 + .../hooks/external/useField/useField.ts | 38 ++++ .../external/useField/useField.unit.test.ts | 143 +++++++++++++ .../{internal => external}/useSubmit/index.ts | 0 .../useSubmit/useSubmit.ts | 0 .../useSubmit/useSubmit.unit.test.ts | 0 .../external/useValueDestination/index.ts | 1 + .../useValueDestination.ts | 13 ++ .../useValueDestination.unit.test.ts | 45 +++++ .../hooks/internal/useFieldHelpers/types.ts | 2 +- .../useFieldHelpers/useFieldHelpers.ts | 5 +- .../useFieldHelpers.unit.test.ts | 13 +- .../useTouched/useTouched.unit.test.ts | 6 +- .../useValidationSchema.unit.test.ts | 8 +- .../organisms/Form/DynamicForm/index.ts | 1 + .../useManualValidate.unit.test.ts | 44 ++-- .../Form/hooks/useRuleEngine/useRuleEngine.ts | 9 +- .../useRuleEngine/useRuleEngine.unit.test.ts | 33 +-- 31 files changed, 829 insertions(+), 84 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts rename packages/ui/src/components/organisms/Form/DynamicForm/hooks/{internal => external}/useSubmit/index.ts (100%) rename packages/ui/src/components/organisms/Form/DynamicForm/hooks/{internal => external}/useSubmit/useSubmit.ts (100%) rename packages/ui/src/components/organisms/Form/DynamicForm/hooks/{internal => external}/useSubmit/useSubmit.unit.test.ts (100%) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/useValueDestination.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/useValueDestination.unit.test.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx index a7a9b0d457..cca3a33118 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -3,8 +3,8 @@ import { FunctionComponent, useMemo } from 'react'; import { Renderer, TRendererSchema } from '../../Renderer'; import { ValidatorProvider } from '../Validator'; import { DynamicFormContext, IDynamicFormContext } from './context'; +import { useSubmit } from './hooks/external/useSubmit'; import { useFieldHelpers } from './hooks/internal/useFieldHelpers'; -import { useSubmit } from './hooks/internal/useSubmit'; import { useTouched } from './hooks/internal/useTouched'; import { useValidationSchema } from './hooks/internal/useValidationSchema'; import { useValues } from './hooks/internal/useValues'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts index 00d99c03cb..9e2abc9739 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react'; import { useContext } from 'react'; -import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { DynamicFormContext } from '../../dynamic-form.context'; import { useDynamicForm } from './useDynamicForm'; @@ -18,7 +18,7 @@ describe('useDynamicForm', () => { beforeEach(() => { vi.clearAllMocks(); - (useContext as Mock).mockReturnValue(mockContextValue); + vi.mocked(useContext).mockReturnValue(mockContextValue); }); it('should call useContext with DynamicFormContext', () => { 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 020d44161a..5d3e76d3d5 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts @@ -1,3 +1,4 @@ +import { IFieldHelpers } from '../hooks/internal/useFieldHelpers/types'; import { ITouchedState } from '../hooks/internal/useTouched'; import { TElementsMap } from '../types'; @@ -5,5 +6,6 @@ export interface IDynamicFormContext { values: TValues; touched: ITouchedState; elementsMap: TElementsMap; + fieldHelpers: IFieldHelpers; submit: () => void; } diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx index d2629c33c0..8ed1d43ac1 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx @@ -2,8 +2,9 @@ import { AnyObject } from '@/common'; import { Button } from '@/components/atoms'; import { Renderer, TRendererSchema } from '@/components/organisms/Renderer'; import { useDynamicForm } from '../../context'; +import { useElement } from '../../hooks/external'; import { TBaseFormElements, TDynamicFormElement } from '../../types'; -import { useFieldList } from './hooks/useFieldList'; +import { IUseFieldParams, useFieldList } from './hooks/useFieldList'; import { StackProvider, useStack } from './providers/StackProvider'; export type TFieldListValueType = T[]; @@ -16,21 +17,30 @@ export interface IFieldListOptions { export const FieldList: TDynamicFormElement< TBaseFormElements, - { addButtonLabel: string; removeButtonLabel: string } + { addButtonLabel: string; removeButtonLabel: string } & IUseFieldParams > = props => { const { elementsMap } = useDynamicForm(); const { stack } = useStack(); const { element } = props; + const { id: fieldId } = useElement(element, stack); const { addButtonLabel = 'Add Item', removeButtonLabel = 'Remove' } = element.params || {}; - const { items, addItem, removeItem } = useFieldList(props); + const { items, addItem, removeItem } = useFieldList({ element }); return ( -
- {items.map((item, index) => { +
+ {items.map((_, index) => { return ( -
+
- removeItem(index)}> + removeItem(index)} + data-testid={`${fieldId}-fieldlist-item-remove-${index}`} + > {removeButtonLabel}
diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx new file mode 100644 index 0000000000..4c271293f9 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx @@ -0,0 +1,127 @@ +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useDynamicForm } from '../../context'; +import { useElement } from '../../hooks/external'; +import { IFormElement } from '../../types'; +import { FieldList } from './FieldList'; +import { IUseFieldParams, useFieldList } from './hooks/useFieldList'; +import { useStack } from './providers/StackProvider'; + +vi.mock('../../context'); +vi.mock('../../hooks/external/useElement'); +vi.mock('./providers/StackProvider'); +vi.mock('./hooks/useFieldList'); +vi.mock('@/components/atoms', () => ({ + Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => ( + + ), +})); +vi.mock('@/components/organisms/Renderer', () => ({ + Renderer: () =>
Renderer
, +})); + +describe('FieldList', () => { + const mockElement = { + id: 'test-field', + valueDestination: 'test.path', + params: { + addButtonLabel: 'Custom Add', + removeButtonLabel: 'Custom Remove', + }, + children: [], + } as unknown as IFormElement< + 'fieldlist', + { addButtonLabel: string; removeButtonLabel: string } & IUseFieldParams + >; + + const mockItems = [{ id: 1 }, { id: 2 }]; + const mockAddItem = vi.fn(); + const mockRemoveItem = vi.fn(); + const mockStack = [0]; + + beforeEach(() => { + cleanup(); + vi.clearAllMocks(); + + vi.mocked(useDynamicForm).mockReturnValue({ + elementsMap: {}, + } as any); + + vi.mocked(useStack).mockReturnValue({ + stack: mockStack, + }); + + vi.mocked(useElement).mockReturnValue({ id: mockElement.id } as any); + + vi.mocked(useFieldList).mockReturnValue({ + items: mockItems, + addItem: mockAddItem, + removeItem: mockRemoveItem, + }); + }); + + describe('test ids', () => { + it('should render field list with correct test id', () => { + render(); + screen.getByTestId(`${mockElement.id}-fieldlist`); + }); + + it('should render field list item with correct test id and indexes', () => { + render(); + screen.getByTestId(`${mockElement.id}-fieldlist-item-0`); + screen.getByTestId(`${mockElement.id}-fieldlist-item-1`); + }); + + it('should render field list item remove button with correct test id and indexes', () => { + render(); + screen.getByTestId(`${mockElement.id}-fieldlist-item-remove-0`); + screen.getByTestId(`${mockElement.id}-fieldlist-item-remove-1`); + }); + }); + + it('should render items with remove buttons and renderers', () => { + render(); + + const removeButtons = screen.getAllByText('Custom Remove'); + expect(removeButtons).toHaveLength(2); + }); + + it('should render add button with custom label', () => { + render(); + + screen.getByText('Custom Add'); + }); + + it('should use default labels when not provided', () => { + const elementWithoutLabels = { + ...mockElement, + params: {}, + } as unknown as IFormElement< + 'fieldlist', + { addButtonLabel: string; removeButtonLabel: string } & IUseFieldParams + >; + + render(); + + screen.getByText('Add Item'); + screen.getAllByText('Remove'); + }); + + it('should call addItem when add button is clicked', () => { + render(); + + const addButton = screen.getByText('Custom Add'); + fireEvent.click(addButton); + + expect(mockAddItem).toHaveBeenCalledTimes(1); + }); + + it('should call removeItem with correct index when remove button is clicked', () => { + render(); + + const removeButtons = screen.getAllByText('Custom Remove'); + fireEvent.click(removeButtons[1]!); + + expect(mockRemoveItem).toHaveBeenCalledWith(1); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts index 21e8e2599a..6400fec2b8 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts @@ -1,41 +1,35 @@ -import { useUIElement } from '@/pages/CollectionFlowV2/hooks/useUIElement'; -import set from 'lodash/set'; -import { useCallback, useMemo } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import { IFormElement } from '../../../../types'; +import { useCallback } from 'react'; +import { useField } from '../../../../hooks/external'; +import { IFormElement, TBaseFormElements } from '../../../../types'; +import { useStack } from '../../providers/StackProvider'; +export interface IUseFieldParams { + defaultValue: T; +} export interface IUseFieldListProps { - element: IFormElement; + element: IFormElement>; } -export const useFieldList = ({}) => { - const uiElement = useUIElement(definition, payload, stack); - - const items = useMemo(() => (uiElement.getValue() as Array<{ _id: string }>) || [], [uiElement]); +export const useFieldList = ({ element }: IUseFieldListProps) => { + const { stack } = useStack(); + const { onChange, value = [] } = useField(element, stack); const addItem = useCallback(() => { - const valueDestination = uiElement.getValueDestination(); - - const newValue = [...items, { _id: uuidv4(), ...options?.defaultValue }]; - set(payload, valueDestination, newValue); - - stateApi.setContext(payload); - }, [uiElement, items, stateApi]); + onChange([...value, element.params?.defaultValue]); + }, [value, element.params?.defaultValue, onChange]); const removeItem = useCallback( (index: number) => { - if (!Array.isArray(items)) return; - - const newValue = items.filter((_, i) => i !== index); - set(payload, uiElement.getValueDestination(), newValue); + if (!Array.isArray(value)) return; - stateApi.setContext(payload); + const newValue = value.filter((_, i) => i !== index); + onChange(newValue); }, - [uiElement, items, stateApi], + [value, onChange], ); return { - items, + items: value, addItem, removeItem, }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.unit.test.ts new file mode 100644 index 0000000000..e0d2469c29 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.unit.test.ts @@ -0,0 +1,83 @@ +import { renderHook } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useField } from '../../../../hooks/external'; +import { IFormElement, TBaseFormElements } from '../../../../types'; +import { useStack } from '../../providers/StackProvider'; +import { IUseFieldParams, useFieldList } from './useFieldList'; + +vi.mock('../../../../hooks/external'); +vi.mock('../../providers/StackProvider'); + +describe('useFieldList', () => { + const mockElement = { + id: 'test', + valueDestination: 'test', + element: 'fieldlist', + params: { + defaultValue: { test: 'value' }, + }, + } as unknown as IFormElement>; + + const mockOnChange = vi.fn(); + const mockStack = [0]; + + beforeEach(() => { + vi.mocked(useStack).mockReturnValue({ stack: mockStack }); + vi.mocked(useField).mockReturnValue({ + onChange: mockOnChange, + value: [], + } as unknown as ReturnType); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should initialize with empty array if no value provided', () => { + const { result } = renderHook(() => useFieldList({ element: mockElement })); + expect(result.current.items).toEqual([]); + }); + + it('should add item with default value', () => { + const { result } = renderHook(() => useFieldList({ element: mockElement })); + + result.current.addItem(); + + expect(mockOnChange).toHaveBeenCalledWith([{ test: 'value' }]); + }); + + it('should remove item at specified index', () => { + const existingItems = [{ test: '1' }, { test: '2' }, { test: '3' }]; + vi.mocked(useField).mockReturnValue({ + onChange: mockOnChange, + value: existingItems, + } as unknown as ReturnType); + + const { result } = renderHook(() => useFieldList({ element: mockElement })); + + result.current.removeItem(1); + + expect(mockOnChange).toHaveBeenCalledWith([{ test: '1' }, { test: '3' }]); + }); + + it('should not remove item if value is not an array', () => { + vi.mocked(useField).mockReturnValue({ + onChange: mockOnChange, + value: 'not-an-array' as any, + touched: false, + onBlur: vi.fn(), + } as unknown as ReturnType); + + const { result } = renderHook(() => useFieldList({ element: mockElement })); + + result.current.removeItem(0); + + expect(mockOnChange).not.toHaveBeenCalled(); + }); + + it('should pass stack to useField', () => { + renderHook(() => useFieldList({ element: mockElement })); + + expect(useField).toHaveBeenCalledWith(mockElement, mockStack); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/index.ts new file mode 100644 index 0000000000..e7c7359a7d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/index.ts @@ -0,0 +1,5 @@ +export * from './useElement'; +export * from './useElementId'; +export * from './useField'; +export * from './useSubmit'; +export * from './useValueDestination'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts index a16b586450..cb224d9e3e 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts @@ -1,5 +1,27 @@ +import { useRuleEngine } from '@/components/organisms/Form/hooks/useRuleEngine'; +import { TDeepthLevelStack } from '@/components/organisms/Form/Validator'; +import { useMemo } from 'react'; +import { useDynamicForm } from '../../../context'; import { IFormElement } from '../../../types'; +import { useElementId } from '../useElementId'; -export const useElement = (element: IFormElement) => { - return {}; +export const useElement = (element: IFormElement, stack: TDeepthLevelStack = []) => { + const { values } = useDynamicForm(); + const hiddenRulesResult = useRuleEngine(values, { + rules: element.hidden, + runOnInitialize: true, + executionDelay: 500, + }); + + const isHidden = useMemo(() => { + if (!hiddenRulesResult.length) return false; + + return hiddenRulesResult.every(result => result.result === true); + }, [hiddenRulesResult]); + + return { + id: useElementId(element, stack), + originId: element.id, + hidden: isHidden, + }; }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts new file mode 100644 index 0000000000..96a5cf1786 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts @@ -0,0 +1,189 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { IDynamicFormContext, useDynamicForm } from '../../../context'; +import { IFormElement } from '../../../types'; +import { useElement } from './useElement'; + +vi.mock('../../../context', () => ({ + useDynamicForm: vi.fn().mockReturnValue({ + values: {}, + } as IDynamicFormContext), +})); + +describe('useElement', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useDynamicForm).mockReturnValue({ + values: { + test: 1, + }, + } as IDynamicFormContext); + + vi.useFakeTimers(); + }); + + describe('when stack not provided', () => { + it('should return unmodified id and origin id', () => { + const element = { id: 'test-id' } as IFormElement; + + const { result } = renderHook(() => useElement(element)); + + expect(result.current.id).toBe('test-id'); + expect(result.current.originId).toBe('test-id'); + }); + }); + + describe('when stack provided', () => { + it('should format id with stack', () => { + const element = { id: 'test-id' } as IFormElement; + const stack = [1, 2]; + + const { result } = renderHook(() => useElement(element, stack)); + + expect(result.current.id).toBe(`${element.id}-1-2`); + expect(result.current.originId).toBe(element.id); + }); + }); + + describe('when hidden rules provided', () => { + it('should return hidden true when all hidden rules return true', () => { + vi.mocked(useDynamicForm).mockReturnValue({ + values: { + test: 1, + }, + } as IDynamicFormContext); + + const element = { + id: 'test-id', + hidden: [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 1] } }], + } as IFormElement; + + const { result } = renderHook(() => useElement(element)); + + expect(result.current.hidden).toBe(true); + }); + + it('should return hidden false when any hidden rule returns false', () => { + const element = { + id: 'test-id', + hidden: [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 5] } }], + } as IFormElement; + + const { result } = renderHook(() => useElement(element)); + + expect(result.current.hidden).toBe(false); + }); + + describe('when rules change', () => { + it('should move from hidden false to hidden true when rules change', async () => { + vi.mocked(useDynamicForm).mockReturnValue({ + values: { + test: 1, + }, + } as IDynamicFormContext); + + const element = { + id: 'test-id', + hidden: [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 5] } }], + } as IFormElement; + + const { result, rerender } = renderHook(() => useElement(element)); + + expect(result.current.hidden).toBe(false); + + element.hidden = [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 1] } }]; + + rerender(); + + await vi.advanceTimersByTimeAsync(500); + + expect(result.current.hidden).toBe(true); + }); + + it('should move from hidden true to hidden false when rules change', async () => { + vi.mocked(useDynamicForm).mockReturnValue({ + values: { + test: 1, + }, + } as IDynamicFormContext); + + const element = { + id: 'test-id', + hidden: [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 1] } }], + } as IFormElement; + + const { result, rerender } = renderHook(() => useElement(element)); + + expect(result.current.hidden).toBe(true); + + element.hidden = [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 5] } }]; + + rerender(); + + await vi.advanceTimersByTimeAsync(500); + + expect(result.current.hidden).toBe(false); + }); + }); + + describe('when values change', () => { + it('should move from hidden true to hidden false when values change', async () => { + vi.mocked(useDynamicForm).mockReturnValue({ + values: { + test: 1, + }, + } as IDynamicFormContext); + + const element = { + id: 'test-id', + hidden: [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 1] } }], + } as IFormElement; + + const { result, rerender } = renderHook(() => useElement(element)); + + expect(result.current.hidden).toBe(true); + + vi.mocked(useDynamicForm).mockReturnValue({ + values: { + test: 5, + }, + } as IDynamicFormContext); + + rerender(); + + await vi.advanceTimersByTimeAsync(500); + + expect(result.current.hidden).toBe(false); + }); + + it('should move from hidden false to hidden true when values change', async () => { + vi.mocked(useDynamicForm).mockReturnValue({ + values: { + test: 1, + }, + } as IDynamicFormContext); + + const element = { + id: 'test-id', + hidden: [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 5] } }], + } as IFormElement; + + const { result, rerender } = renderHook(() => useElement(element)); + + expect(result.current.hidden).toBe(false); + + vi.mocked(useDynamicForm).mockReturnValue({ + values: { + test: 5, + }, + } as IDynamicFormContext); + + rerender(); + + await vi.advanceTimersByTimeAsync(500); + + expect(result.current.hidden).toBe(true); + }); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/index.ts new file mode 100644 index 0000000000..24e03d16aa --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/index.ts @@ -0,0 +1 @@ +export * from './useElementId'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.ts new file mode 100644 index 0000000000..98864639cf --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.ts @@ -0,0 +1,10 @@ +import { TDeepthLevelStack } from '@/components/organisms/Form/Validator'; +import { formatId } from '@/components/organisms/Form/Validator/utils/format-id'; +import { useMemo } from 'react'; +import { IFormElement } from '../../../types'; + +export const useElementId = (element: IFormElement, stack: TDeepthLevelStack = []) => { + const formattedId = useMemo(() => formatId(element.id, stack), [element.id, stack]); + + return formattedId; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.unit.test.ts new file mode 100644 index 0000000000..5ba4888d7e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.unit.test.ts @@ -0,0 +1,36 @@ +import { renderHook } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { IFormElement } from '../../../types'; +import { useElementId } from './useElementId'; + +describe('useElementId', () => { + describe('when stack not provided', () => { + it('should return unmodified id', () => { + const element = { id: 'test-id' } as IFormElement; + + const { result } = renderHook(() => useElementId(element)); + + expect(result.current).toBe('test-id'); + }); + }); + + describe('when stack provided', () => { + it('should format id with stack', () => { + const element = { id: 'test-id' } as IFormElement; + const stack = [1, 2]; + + const { result } = renderHook(() => useElementId(element, stack)); + + expect(result.current).toBe('test-id-1-2'); + }); + + it('should format id with empty stack', () => { + const element = { id: 'test-id' } as IFormElement; + const stack: number[] = []; + + const { result } = renderHook(() => useElementId(element, stack)); + + expect(result.current).toBe('test-id'); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/index.ts new file mode 100644 index 0000000000..47c240b7c1 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/index.ts @@ -0,0 +1 @@ +export * from './useField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts new file mode 100644 index 0000000000..88aa69cbf2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts @@ -0,0 +1,38 @@ +import { TDeepthLevelStack } from '@/components/organisms/Form/Validator'; +import { useCallback, useMemo } from 'react'; +import { useDynamicForm } from '../../../context'; +import { IFormElement } from '../../../types'; +import { useElementId } from '../useElementId'; +import { useValueDestination } from '../useValueDestination'; + +export const useField = (element: IFormElement, stack: TDeepthLevelStack = []) => { + const fieldId = useElementId(element, stack); + const valueDestination = useValueDestination(element, stack); + const { fieldHelpers } = useDynamicForm(); + + const { setValue, getValue, setTouched, getTouched } = fieldHelpers; + + const value = useMemo(() => getValue(valueDestination), [valueDestination, getValue]); + const touched = useMemo(() => getTouched(fieldId), [fieldId, getTouched]); + + const onChange = useCallback( + (value: TValue) => { + setValue(fieldId, valueDestination, value); + setTouched(fieldId, true); + + // TODO: Dispatch onChange event? + }, + [fieldId, valueDestination, setValue, setTouched], + ); + + const onBlur = useCallback(() => { + // TODO: Dispatch onBlur event? + }, []); + + return { + value, + touched, + onChange, + onBlur, + }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts new file mode 100644 index 0000000000..d1b21a6aa7 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts @@ -0,0 +1,143 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { IDynamicFormContext, useDynamicForm } from '../../../context'; +import { IFormElement } from '../../../types'; +import { useElementId } from '../useElementId'; +import { useValueDestination } from '../useValueDestination'; +import { useField } from './useField'; + +vi.mock('../../../context', () => ({ + useDynamicForm: vi.fn(), +})); + +vi.mock('../useElementId', () => ({ + useElementId: vi.fn(), +})); + +vi.mock('../useValueDestination', () => ({ + useValueDestination: vi.fn(), +})); + +describe('useField', () => { + const mockElement = { + id: 'test-field', + valueDestination: 'test.path', + } as IFormElement; + + const mockStack = [1, 2]; + + const mockSetValue = vi.fn(); + const mockGetValue = vi.fn(); + const mockSetTouched = vi.fn(); + const mockGetTouched = vi.fn(); + + const mockFieldHelpers = { + setValue: mockSetValue, + getValue: mockGetValue, + setTouched: mockSetTouched, + getTouched: mockGetTouched, + }; + + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(useElementId).mockReturnValue('test-field-1-2'); + vi.mocked(useValueDestination).mockReturnValue('test.path[1][2]'); + vi.mocked(useDynamicForm).mockReturnValue({ + fieldHelpers: mockFieldHelpers, + } as unknown as IDynamicFormContext); + mockGetValue.mockReturnValue('test-value'); + mockGetTouched.mockReturnValue(false); + }); + + it('should return field state and handlers', () => { + const { result } = renderHook(() => useField(mockElement, mockStack)); + + expect(result.current).toEqual({ + value: 'test-value', + touched: false, + onChange: expect.any(Function), + onBlur: expect.any(Function), + }); + }); + + it('should call useElementId with element and stack', () => { + renderHook(() => useField(mockElement, mockStack)); + + expect(useElementId).toHaveBeenCalledWith(mockElement, mockStack); + }); + + it('should call useValueDestination with element and stack', () => { + renderHook(() => useField(mockElement, mockStack)); + + expect(useValueDestination).toHaveBeenCalledWith(mockElement, mockStack); + }); + + it('should get value using valueDestination', () => { + renderHook(() => useField(mockElement, mockStack)); + + expect(mockGetValue).toHaveBeenCalledWith('test.path[1][2]'); + }); + + it('should get touched state using fieldId', () => { + renderHook(() => useField(mockElement, mockStack)); + + expect(mockGetTouched).toHaveBeenCalledWith('test-field-1-2'); + }); + + describe('onChange', () => { + it('should update value and touched state', () => { + const { result } = renderHook(() => useField(mockElement, mockStack)); + + result.current.onChange('new-value'); + + expect(mockSetValue).toHaveBeenCalledWith('test.path[1][2]', 'new-value'); + expect(mockSetTouched).toHaveBeenCalledWith('test-field-1-2', true); + }); + }); + + describe('when stack is not provided', () => { + it('should use empty array as default stack', () => { + renderHook(() => useField(mockElement)); + + expect(useElementId).toHaveBeenCalledWith(mockElement, []); + expect(useValueDestination).toHaveBeenCalledWith(mockElement, []); + }); + }); + + it('should memoize value', () => { + const { result, rerender } = renderHook(() => useField(mockElement, mockStack)); + const initialValue = result.current.value; + + rerender(); + + expect(result.current.value).toBe(initialValue); + }); + + it('should memoize touched', () => { + const { result, rerender } = renderHook(() => useField(mockElement, mockStack)); + const initialTouched = result.current.touched; + + rerender(); + + expect(result.current.touched).toBe(initialTouched); + }); + + it('should memoize onChange', () => { + const { result, rerender } = renderHook(() => useField(mockElement, mockStack)); + const initialOnChange = result.current.onChange; + + rerender(); + + expect(result.current.onChange).toBe(initialOnChange); + }); + + it('should memoize onBlur', () => { + const { result, rerender } = renderHook(() => useField(mockElement, mockStack)); + const initialOnBlur = result.current.onBlur; + + rerender(); + + expect(result.current.onBlur).toBe(initialOnBlur); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useSubmit/index.ts similarity index 100% rename from packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/index.ts rename to packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useSubmit/index.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useSubmit/useSubmit.ts similarity index 100% rename from packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.ts rename to packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useSubmit/useSubmit.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useSubmit/useSubmit.unit.test.ts similarity index 100% rename from packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useSubmit/useSubmit.unit.test.ts rename to packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useSubmit/useSubmit.unit.test.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/index.ts new file mode 100644 index 0000000000..a1aa065f34 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/index.ts @@ -0,0 +1 @@ +export * from './useValueDestination'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/useValueDestination.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/useValueDestination.ts new file mode 100644 index 0000000000..851a4b7b53 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/useValueDestination.ts @@ -0,0 +1,13 @@ +import { TDeepthLevelStack } from '@/components/organisms/Form/Validator'; +import { formatValueDestination } from '@/components/organisms/Form/Validator/utils/format-value-destination'; +import { useMemo } from 'react'; +import { IFormElement } from '../../../types'; + +export const useValueDestination = (element: IFormElement, stack: TDeepthLevelStack = []) => { + const valueDestination = useMemo( + () => formatValueDestination(element.valueDestination, stack), + [element.valueDestination, stack], + ); + + return valueDestination; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/useValueDestination.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/useValueDestination.unit.test.ts new file mode 100644 index 0000000000..5492e60486 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useValueDestination/useValueDestination.unit.test.ts @@ -0,0 +1,45 @@ +import { renderHook } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { IFormElement } from '../../../types'; +import { useValueDestination } from './useValueDestination'; + +describe('useValueDestination', () => { + describe('when stack not provided', () => { + it('should return unmodified valueDestination', () => { + const element = { valueDestination: 'test.path' } as IFormElement; + + const { result } = renderHook(() => useValueDestination(element)); + + expect(result.current).toBe('test.path'); + }); + }); + + describe('when stack provided', () => { + it('should format valueDestination with stack', () => { + const element = { valueDestination: 'test[$0].path[$1]' } as IFormElement; + const stack = [1, 2]; + + const { result } = renderHook(() => useValueDestination(element, stack)); + + expect(result.current).toBe('test[1].path[2]'); + }); + + it('should format valueDestination with empty stack', () => { + const element = { valueDestination: 'test[$0].path[$1]' } as IFormElement; + const stack: number[] = []; + + const { result } = renderHook(() => useValueDestination(element, stack)); + + expect(result.current).toBe('test[$0].path[$1]'); + }); + + it('should format valueDestination with partial stack usage', () => { + const element = { valueDestination: 'test[$0].path[$1]' } as IFormElement; + const stack = [1, 2]; + + const { result } = renderHook(() => useValueDestination(element, stack)); + + expect(result.current).toBe('test[1].path[2]'); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/types.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/types.ts index 637c07cda8..8acc1097d0 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/types.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/types.ts @@ -2,5 +2,5 @@ export interface IFieldHelpers { getTouched: (fieldId: string) => boolean; getValue: (fieldId: string) => T; setTouched: (fieldId: string, touched: boolean) => void; - setValue: (fieldId: string, value: T) => void; + setValue: (fieldId: string, valueDestination: string, value: T) => void; } diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.ts index 860ca8b6fe..93e84dea96 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.ts @@ -1,3 +1,4 @@ +import get from 'lodash/get'; import { useCallback, useMemo } from 'react'; import { useTouched } from '../useTouched'; import { useValues } from '../useValues'; @@ -19,8 +20,8 @@ export const useFieldHelpers = ({ valuesApi, touchedApi }: IUseFieldHelpersParam ); const getValue = useCallback( - (fieldId: string) => { - return values[fieldId as keyof typeof values] as T; + (valueDestination: string) => { + return get(values, valueDestination) as T; }, [values], ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.unit.test.ts index d0906fa6ff..070a5c5b21 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useFieldHelpers/useFieldHelpers.unit.test.ts @@ -20,6 +20,9 @@ describe('useFieldHelpers', () => { values: { field1: 'value1', field2: 'value2', + nestedValue: { + nestedField1: 'nestedValue1', + }, }, setFieldValue: mockSetFieldValue, }; @@ -68,6 +71,12 @@ describe('useFieldHelpers', () => { expect(result.current.getValue('field2')).toBe('value2'); }); + it('getValue should return correct nested value', () => { + const { result } = setup(); + + expect(result.current.getValue('nestedValue.nestedField1')).toBe('nestedValue1'); + }); + it('setTouched should call touchedApi.setFieldTouched', () => { const { result } = setup(); @@ -80,10 +89,10 @@ describe('useFieldHelpers', () => { it('setValue should call valuesApi.setFieldValue', () => { const { result } = setup(); - result.current.setValue('field1', 'field1', 'newValue'); + result.current.setValue('field1', 'path.to.field', 'newValue'); expect(mockSetFieldValue).toHaveBeenCalledTimes(1); - expect(mockSetFieldValue).toHaveBeenCalledWith('field1', 'field1', 'newValue'); + expect(mockSetFieldValue).toHaveBeenCalledWith('field1', 'path.to.field', 'newValue'); }); it('should memoize helper functions', () => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts index 106dfa589c..9d8096fb01 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts @@ -1,5 +1,5 @@ import { act, renderHook } from '@testing-library/react'; -import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { IFormElement } from '../../../types'; import { generateTouchedMapForAllElements } from './helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements'; import { useTouched } from './useTouched'; @@ -57,7 +57,7 @@ describe('useTouched', () => { it('should touch all fields', () => { const mockTouchedMap = { '1': true, '2': true }; - (generateTouchedMapForAllElements as Mock).mockReturnValue(mockTouchedMap); + vi.mocked(generateTouchedMapForAllElements).mockReturnValue(mockTouchedMap); const { result } = renderHook(() => useTouched(elements, context)); @@ -73,7 +73,7 @@ describe('useTouched', () => { const mockTouchedMap1 = { '1': true }; const mockTouchedMap2 = { '1': true, '2': true }; - (generateTouchedMapForAllElements as Mock) + vi.mocked(generateTouchedMapForAllElements) .mockReturnValueOnce(mockTouchedMap1) .mockReturnValueOnce(mockTouchedMap2); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.unit.test.ts index 143a73440f..ed87de317e 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.unit.test.ts @@ -1,10 +1,10 @@ import { renderHook } from '@testing-library/react'; -import { beforeEach, describe, expect, Mock, test, vi } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { convertFormElementsToValidationSchema } from '../../../helpers/convert-form-emenents-to-validation-schema'; import { IFormElement } from '../../../types'; import { useValidationSchema } from './useValidationSchema'; -vi.mock('../../helpers/convert-form-emenents-to-validation-schema', () => ({ +vi.mock('../../../helpers/convert-form-emenents-to-validation-schema', () => ({ convertFormElementsToValidationSchema: vi.fn(), })); @@ -28,7 +28,7 @@ describe('useValidationSchema', () => { beforeEach(() => { vi.clearAllMocks(); - (convertFormElementsToValidationSchema as Mock).mockReturnValue(mockValidationSchema); + vi.mocked(convertFormElementsToValidationSchema).mockReturnValue(mockValidationSchema); }); test('should return validation schema', () => { @@ -75,7 +75,7 @@ describe('useValidationSchema', () => { }, ]; - (convertFormElementsToValidationSchema as Mock).mockReturnValue(newValidationSchema); + vi.mocked(convertFormElementsToValidationSchema).mockReturnValue(newValidationSchema); // Rerender with different props rerender(newElements); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/index.ts index 2a4d8a96c8..5b26b998f6 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/index.ts @@ -1 +1,2 @@ export * from './DynamicForm'; +export * from './hooks/external'; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.unit.test.ts index b89e53d0d8..48440ff59d 100644 --- a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useManualValidate.unit.test.ts @@ -4,14 +4,7 @@ import { validate } from '../../../utils/validate'; import { useManualValidate } from './useManualValidate'; vi.mock('../../../utils/validate', () => ({ - validate: vi.fn().mockReturnValue([ - { - id: 'name', - originId: 'name', - message: ['error'], - invalidValue: 'John', - }, - ]), + validate: vi.fn(), })); describe('useManualValidate', () => { @@ -24,19 +17,32 @@ describe('useManualValidate', () => { it('should initialize with empty validation errors', () => { const { result } = renderHook(() => useManualValidate(mockContext, mockSchema)); + const [validationErrors, validate] = result.current; - expect(result.current.validationErrors).toEqual([]); + expect(validationErrors).toEqual([]); + expect(validate).toBeInstanceOf(Function); }); it('should validate and set errors when validate is called', () => { const { result } = renderHook(() => useManualValidate(mockContext, mockSchema)); + vi.mocked(validate).mockReturnValue([ + { + id: 'name', + originId: 'name', + message: ['error'], + invalidValue: 'John', + }, + ]); + act(() => { - result.current.validate(); + result.current[1](); }); - expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: false }); - expect(result.current.validationErrors).toEqual([ + expect(vi.mocked(validate)).toHaveBeenCalledWith(mockContext, mockSchema, { + abortEarly: false, + }); + expect(result.current[0]).toEqual([ { id: 'name', originId: 'name', @@ -52,10 +58,12 @@ describe('useManualValidate', () => { ); act(() => { - result.current.validate(); + result.current[1](); }); - expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: true }); + expect(vi.mocked(validate)).toHaveBeenCalledWith(mockContext, mockSchema, { + abortEarly: true, + }); }); it('should memoize validate callback with correct dependencies', () => { @@ -70,7 +78,9 @@ describe('useManualValidate', () => { }, ); - const firstValidate = result.current.validate; + const [_, validate] = result.current; + + const firstValidate = validate; // Rerender with same props rerender({ @@ -79,7 +89,7 @@ describe('useManualValidate', () => { params: { abortEarly: false }, }); - expect(result.current.validate).toBe(firstValidate); + expect(validate).toBe(firstValidate); // Rerender with different context rerender({ @@ -88,6 +98,6 @@ describe('useManualValidate', () => { params: { abortEarly: false }, }); - expect(result.current.validate).not.toBe(firstValidate); + expect(result.current[1]).not.toBe(firstValidate); }); }); diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.ts index 267ac0cfae..2d8b04e045 100644 --- a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.ts +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.ts @@ -4,7 +4,7 @@ import { IRule, IRuleExecutionResult, TRuleEngine } from './types'; import { executeRules } from './utils/execute-rules'; export interface IRuleEngineParams { - rules: Array> | IRule; + rules?: Array> | IRule; executeRulesSync?: boolean; runOnInitialize?: boolean; executionDelay?: number; @@ -20,11 +20,14 @@ export const useRuleEngine = ( IRuleExecutionResult[] >(() => runOnInitialize && !executeRulesSync - ? executeRules(context, Array.isArray(_rules) ? _rules : [_rules]) + ? executeRules( + context, + Array.isArray(_rules) ? _rules?.filter(Boolean) : _rules ? [_rules] : [], + ) : [], ); - const rules = useMemo(() => (Array.isArray(_rules) ? _rules : [_rules]), [_rules]); + const rules = useMemo(() => (Array.isArray(_rules) ? _rules : _rules ? [_rules] : []), [_rules]); const syncRuleEngineExecutionResults = useMemo(() => { if (!executeRulesSync) return []; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts index 234d886085..ffd5c850c6 100644 --- a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts @@ -1,5 +1,6 @@ import { renderHook } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { IRule, IRuleExecutionResult } from './types'; import { useRuleEngine } from './useRuleEngine'; import { executeRules } from './utils/execute-rules'; @@ -20,10 +21,10 @@ describe('useRuleEngine', () => { it('should execute rules synchronously when executeRulesSync is true', () => { // Arrange const context = { foo: 'bar' }; - const rules = [{ engine: 'json-logic', value: true }]; - const expectedResults = [{ rule: rules[0], result: true }]; + const rules: IRule[] = [{ engine: 'json-logic', value: true }]; + const expectedResults: IRuleExecutionResult[] = [{ rule: rules[0] as IRule, result: true }]; - (executeRules as Mock).mockReturnValue(expectedResults); + vi.mocked(executeRules).mockReturnValue(expectedResults); // Act const { result } = renderHook(() => useRuleEngine(context, { rules, executeRulesSync: true })); @@ -36,10 +37,10 @@ describe('useRuleEngine', () => { it('should execute rules asynchronously when executeRulesSync is false', async () => { // Arrange const context = { foo: 'bar' }; - const rules = [{ engine: 'json-logic', value: true }]; - const expectedResults = [{ rule: rules[0], result: true }]; + const rules: IRule[] = [{ engine: 'json-logic', value: true }]; + const expectedResults: IRuleExecutionResult[] = [{ rule: rules[0] as IRule, result: true }]; - (executeRules as Mock).mockReturnValue(expectedResults); + vi.mocked(executeRules).mockReturnValue(expectedResults); // Act const { result } = renderHook(() => useRuleEngine(context, { rules, executeRulesSync: false })); @@ -55,10 +56,10 @@ describe('useRuleEngine', () => { it('should execute rules on initialize when runOnInitialize is true', () => { // Arrange const context = { foo: 'bar' }; - const rules = [{ engine: 'json-logic', value: true }]; - const expectedResults = [{ rule: rules[0], result: true }]; + const rules: IRule[] = [{ engine: 'json-logic', value: true }]; + const expectedResults: IRuleExecutionResult[] = [{ rule: rules[0] as IRule, result: true }]; - (executeRules as Mock).mockReturnValue(expectedResults); + vi.mocked(executeRules).mockReturnValue(expectedResults); // Act const { result } = renderHook(() => useRuleEngine(context, { rules, runOnInitialize: true })); @@ -71,10 +72,10 @@ describe('useRuleEngine', () => { it('should convert single rule to array', () => { // Arrange const context = { foo: 'bar' }; - const rule = { engine: 'json-logic', value: true }; - const expectedResults = [{ rule, result: true }]; + const rule: IRule = { engine: 'json-logic', value: true }; + const expectedResults: IRuleExecutionResult[] = [{ rule, result: true }]; - (executeRules as Mock).mockReturnValue(expectedResults); + vi.mocked(executeRules).mockReturnValue(expectedResults); // Act const { result } = renderHook(() => @@ -89,11 +90,11 @@ describe('useRuleEngine', () => { it('should use custom execution delay', async () => { // Arrange const context = { foo: 'bar' }; - const rules = [{ engine: 'json-logic', value: true }]; + const rules: IRule[] = [{ engine: 'json-logic', value: true }]; const customDelay = 1000; - const expectedResults = [{ rule: rules[0], result: true }]; + const expectedResults: IRuleExecutionResult[] = [{ rule: rules[0] as IRule, result: true }]; - (executeRules as Mock).mockReturnValue(expectedResults); + vi.mocked(executeRules).mockReturnValue(expectedResults); // Act const { result } = renderHook(() => From 5b175feddf48f59ab2f20fc8b5e256167dc5cde8 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 13 Dec 2024 14:37:15 +0200 Subject: [PATCH 12/54] fix: tests --- .../DynamicForm/hooks/external/useField/useField.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts index d1b21a6aa7..6bf6389ad7 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts @@ -91,7 +91,7 @@ describe('useField', () => { result.current.onChange('new-value'); - expect(mockSetValue).toHaveBeenCalledWith('test.path[1][2]', 'new-value'); + expect(mockSetValue).toHaveBeenCalledWith('test-field-1-2', 'test.path[1][2]', 'new-value'); expect(mockSetTouched).toHaveBeenCalledWith('test-field-1-2', true); }); }); From 0d94cb00f0f1a924eee3f20a69cfab6c72b3b411 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 13 Dec 2024 14:42:32 +0200 Subject: [PATCH 13/54] fix: build --- .../components/organisms/Form/DynamicForm/DynamicForm.tsx | 2 +- packages/ui/src/components/organisms/Form/hooks/index.ts | 1 + packages/ui/src/components/organisms/Form/index.ts | 3 +++ packages/ui/src/components/organisms/index.ts | 5 +++-- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/hooks/index.ts create mode 100644 packages/ui/src/components/organisms/Form/index.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx index cca3a33118..a1aefca7b2 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -10,7 +10,7 @@ import { useValidationSchema } from './hooks/internal/useValidationSchema'; import { useValues } from './hooks/internal/useValues'; import { IDynamicFormProps } from './types'; -export const DynamicForm: FunctionComponent = ({ +export const DynamicFormV2: FunctionComponent = ({ elements, values: initialValues, validationParams, diff --git a/packages/ui/src/components/organisms/Form/hooks/index.ts b/packages/ui/src/components/organisms/Form/hooks/index.ts new file mode 100644 index 0000000000..be807bc144 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/hooks/index.ts @@ -0,0 +1 @@ +export * from './useRuleEngine'; diff --git a/packages/ui/src/components/organisms/Form/index.ts b/packages/ui/src/components/organisms/Form/index.ts new file mode 100644 index 0000000000..8a6018b1da --- /dev/null +++ b/packages/ui/src/components/organisms/Form/index.ts @@ -0,0 +1,3 @@ +export * from './DynamicForm'; +export * from './hooks'; +export * from './Validator'; diff --git a/packages/ui/src/components/organisms/index.ts b/packages/ui/src/components/organisms/index.ts index 761a953424..6efad98e13 100644 --- a/packages/ui/src/components/organisms/index.ts +++ b/packages/ui/src/components/organisms/index.ts @@ -1,3 +1,4 @@ -export * from './WorkflowsTable'; -export * from './DynamicForm'; export * from './DataTable'; +export * from './DynamicForm'; +export * from './Form'; +export * from './WorkflowsTable'; From b631effaab43d6337d5cb47b735c845713e65c79 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 13 Dec 2024 17:09:01 +0200 Subject: [PATCH 14/54] feat: added input boilerplates(unfinished) & field layout & tests --- packages/ui/package.json | 2 + .../controls/SubmitButton/SubmitButton.tsx | 39 + .../SubmitButton/SubmitButton.unit.test.tsx | 122 + .../controls/SubmitButton/index.ts | 1 + .../AutocompleteField/AutocompleteField.tsx | 35 + .../fields/AutocompleteField/index.ts | 1 + .../fields/CheckboxField/CheckboxField.tsx | 35 + .../DynamicForm/fields/CheckboxField/index.ts | 1 + .../fields/CheckboxList/CheckboxList.tsx | 53 + .../DynamicForm/fields/CheckboxList/index.ts | 1 + .../fields/CountryField/CountryField.tsx | 39 + .../DynamicForm/fields/CountryField/index.ts | 1 + .../fields/DateField/DateField.tsx | 65 + .../DynamicForm/fields/DateField/index.ts | 1 + .../fields/DocumentField/DocumentField.tsx | 92 + .../DocumentField/hooks/useDocument/index.ts | 1 + .../hooks/useDocument/useDocument.ts | 98 + .../hooks/useDocumentUpload/index.ts | 1 + .../useDocumentUpload/useDocumentUpload.ts | 46 + .../DocumentField/hooks/useDocuments/index.ts | 1 + .../hooks/useDocuments/useDocuments.ts | 10 + .../DynamicForm/fields/DocumentField/index.ts | 1 + .../IndustriesField/IndustriesField.tsx | 45 + .../fields/IndustriesField/index.ts | 1 + .../fields/LocaleField/LocaleField.tsx | 51 + .../DynamicForm/fields/LocaleField/index.ts | 1 + .../DynamicForm/fields/MCCField/MCCField.tsx | 35 + .../Form/DynamicForm/fields/MCCField/index.ts | 1 + .../DynamicForm/fields/MCCField/options.ts | 3941 +++++++++++++++++ .../MultiselectField/MultiselectField.tsx | 71 + .../fields/MultiselectField/index.ts | 1 + .../NationalityField/NationalityField.tsx | 46 + .../fields/NationalityField/index.ts | 1 + .../fields/PhoneField/PhoneField.tsx | 42 + .../DynamicForm/fields/PhoneField/index.ts | 1 + .../RelationshipField/RelationshipField.tsx | 50 + .../fields/RelationshipField/index.ts | 1 + .../RelationshipField/relationship-options.ts | 31 + .../fields/StateField/StateField.tsx | 57 + .../DynamicForm/fields/StateField/index.ts | 1 + .../fields/TagsField/TagsField.tsx | 45 + .../DynamicForm/fields/TagsField/index.ts | 1 + .../fields/TextField/TextField.tsx | 53 + .../DynamicForm/fields/TextField/helpers.ts | 15 + .../DynamicForm/fields/TextField/index.ts | 1 + .../hooks/external/useField/useField.ts | 16 +- .../external/useField/useField.unit.test.ts | 46 +- .../check-if-required/check-if-required.ts | 26 + .../check-if-required.unit.test.ts | 120 + .../helpers/check-if-required/index.ts | 1 + .../hooks/external/useRequired/index.ts | 1 + .../hooks/external/useRequired/useRequired.ts | 9 + .../useRequired/useRequired.unit.test.ts | 56 + .../layouts/FieldErrors/FieldErrors.tsx | 25 + .../DynamicForm/layouts/FieldErrors/index.ts | 1 + .../layouts/FieldLayout/FieldLayout.tsx | 34 + .../FieldLayout/FieldLayout.unit.test.tsx | 103 + .../DynamicForm/layouts/FieldLayout/index.ts | 1 + .../organisms/Form/DynamicForm/types/index.ts | 15 +- .../organisms/Form/Validator/types/index.ts | 1 + packages/ui/src/setupTests.ts | 15 +- packages/ui/vite.config.ts | 4 +- 62 files changed, 5604 insertions(+), 8 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/CountryField/CountryField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/CountryField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/DocumentField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocument/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocument/useDocument.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocuments/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocuments/useDocuments.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/IndustriesField/IndustriesField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/IndustriesField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/LocaleField/LocaleField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/LocaleField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/MCCField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/options.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/NationalityField/NationalityField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/NationalityField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/RelationshipField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/relationship-options.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/StateField/StateField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/StateField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/helpers.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/useRequired.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/useRequired.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldErrors/FieldErrors.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldErrors/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/index.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index d8b1f175aa..0bb7825894 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -82,7 +82,9 @@ "@storybook/react-vite": "^7.0.26", "@storybook/testing-library": "^0.0.14-next.2", "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^13.3.0", + "@testing-library/user-event": "^14.5.2", "@types/json-logic-js": "^2.0.1", "@types/jsoneditor": "^9.9.5", "@types/lodash": "^4.14.191", 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 new file mode 100644 index 0000000000..b8faa13bc1 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.tsx @@ -0,0 +1,39 @@ +import { Button, useElement } from '@ballerine/ui'; +import { useMemo } from 'react'; +import { useValidator } from '../../../Validator'; +import { useDynamicForm } from '../../context'; +import { useField } from '../../hooks/external'; +import { TBaseFormElements, TDynamicFormElement } from '../../types'; + +export interface ISubmitButtonParams { + disableWhenFormIsInvalid?: boolean; + text?: string; +} + +export const SubmitButton: TDynamicFormElement = ({ + element, +}) => { + const { id } = useElement(element); + const { disabled: _disabled } = useField(element); + const { submit } = useDynamicForm(); + const { isValid } = useValidator(); + + const { disableWhenFormIsInvalid = false, text = 'Submit' } = element.params || {}; + + const disabled = useMemo(() => { + if (disableWhenFormIsInvalid && !isValid) return true; + + return _disabled; + }, [disableWhenFormIsInvalid, isValid, _disabled]); + + return ( + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx new file mode 100644 index 0000000000..d451d5034b --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx @@ -0,0 +1,122 @@ +import { Button } from '@ballerine/ui'; +import '@testing-library/jest-dom'; +import { cleanup, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useValidator } from '../../../Validator'; +import { useDynamicForm } from '../../context'; +import { useField } from '../../hooks/external'; +import { IFormElement, TBaseFormElements } from '../../types'; +import { ISubmitButtonParams, SubmitButton } from './SubmitButton'; + +vi.mock('@ballerine/ui', () => ({ + Button: vi.fn(), + useElement: vi.fn().mockReturnValue({ id: 'test-id' }), +})); + +vi.mock('../../../Validator', () => ({ + useValidator: vi.fn(), +})); + +vi.mock('../../context', () => ({ + useDynamicForm: vi.fn(), +})); + +vi.mock('../../hooks/external', () => ({ + useField: vi.fn(), +})); + +describe('SubmitButton', () => { + const mockElement = { + id: 'test-button', + params: {}, + valueDestination: 'test.path', + element: '' as TBaseFormElements, + } as IFormElement; + + const mockSubmit = vi.fn(); + + beforeEach(() => { + cleanup(); + vi.clearAllMocks(); + + vi.mocked(Button).mockImplementation(({ children, ...props }) => ( + + )); + + vi.mocked(useField).mockReturnValue({ disabled: false } as any); + vi.mocked(useDynamicForm).mockReturnValue({ submit: mockSubmit } as any); + vi.mocked(useValidator).mockReturnValue({ isValid: true } as any); + }); + + it('should render button with default text', () => { + render(); + + screen.getByText('Submit'); + }); + + it('should render button with custom text', () => { + const elementWithText = { + ...mockElement, + params: { text: 'Custom Submit' }, + }; + + render(); + + screen.getByText('Custom Submit'); + }); + + it('should call submit when clicked', async () => { + render(); + + await userEvent.click(screen.getByRole('button')); + + expect(mockSubmit).toHaveBeenCalled(); + }); + + describe('disabled state', () => { + it('should be disabled when useField returns disabled true', () => { + vi.mocked(useField).mockReturnValue({ disabled: true } as any); + + render(); + + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('should be disabled when form is invalid and disableWhenFormIsInvalid is true', () => { + vi.mocked(useValidator).mockReturnValue({ isValid: false } as any); + + const elementWithDisable = { + ...mockElement, + params: { disableWhenFormIsInvalid: true }, + }; + + render(); + + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('should not be disabled when form is invalid but disableWhenFormIsInvalid is false', () => { + vi.mocked(useValidator).mockReturnValue({ isValid: false } as any); + vi.mocked(useField).mockReturnValue({ disabled: false } as any); + + render(); + + expect(screen.getByRole('button')).not.toBeDisabled(); + }); + }); + + it('should have correct test id', () => { + render(); + + expect(screen.getByTestId('test-id-submit-button')).toBeInTheDocument(); + }); + + it('shold call submit when clicked', async () => { + render(); + + await userEvent.click(screen.getByRole('button')); + + expect(mockSubmit).toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/index.ts new file mode 100644 index 0000000000..bcd94ff82e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/index.ts @@ -0,0 +1 @@ +export * from './SubmitButton'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx new file mode 100644 index 0000000000..3ca556c7a3 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx @@ -0,0 +1,35 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { AutocompleteInput, createTestId } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export interface IAutocompleteFieldOption { + label: string; + value: string; +} + +export interface IAutocompleteFieldOptions { + placeholder?: string; + options: IAutocompleteFieldOption[]; +} + +export const AutocompleteField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options: _options, stack }) => { + const { value, onChange, onBlur, disabled } = fieldProps; + const { options = [], placeholder = '' } = _options; + + return ( + + onChange(event.target.value || '')} + onBlur={onBlur} + /> + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/index.ts new file mode 100644 index 0000000000..e17490b84b --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/index.ts @@ -0,0 +1 @@ +export * from './AutocompleteField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx new file mode 100644 index 0000000000..618ed66e38 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx @@ -0,0 +1,35 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { Checkbox, createTestId } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export interface ICheckboxFieldOptions { + label: string; +} + +export const CheckboxField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options, stack }) => { + const { value, onChange, onBlur, disabled } = fieldProps; + const { label } = options; + + return ( + + { + onChange(Boolean(e)); + }} + onBlur={onBlur} + /> + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/index.ts new file mode 100644 index 0000000000..032e30e3a8 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/index.ts @@ -0,0 +1 @@ +export * from './CheckboxField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx new file mode 100644 index 0000000000..ffb97b34bf --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx @@ -0,0 +1,53 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { Checkbox, createTestId, ctw } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export interface ICheckboxListOption { + label: string; + value: string; +} + +export interface ICheckboxListFieldOptions { + options: ICheckboxListOption[]; +} + +export const CheckboxListField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options: _options, stack }) => { + const { value = [], onChange, disabled } = fieldProps; + const { options = [] } = _options || {}; + + return ( + +
+ {options.map(option => ( + + ))} +
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/index.ts new file mode 100644 index 0000000000..e6d5425198 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/index.ts @@ -0,0 +1 @@ +export * from './CheckboxList'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CountryField/CountryField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CountryField/CountryField.tsx new file mode 100644 index 0000000000..1696fed2b4 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CountryField/CountryField.tsx @@ -0,0 +1,39 @@ +import { getCountries } from '@/helpers/countries-data'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { DropdownInput } from '@ballerine/ui'; +import { FunctionComponent, useMemo } from 'react'; + +export interface ICountryFieldOptions { + placeholder?: string; +} + +export const CountryField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options, stack }) => { + const { value, disabled, onChange, onBlur } = fieldProps; + const { placeholder = '' } = options || {}; + + const dropdownOptions = useMemo(() => { + return getCountries().map(country => ({ + value: country.const, + label: country.title, + })); + }, []); + + return ( + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CountryField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CountryField/index.ts new file mode 100644 index 0000000000..7b6770fb65 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CountryField/index.ts @@ -0,0 +1 @@ +export * from './CountryField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx new file mode 100644 index 0000000000..3cbffd78a3 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx @@ -0,0 +1,65 @@ +import { FieldErrors } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { + createTestId, + DatePickerChangeEvent, + DatePickerInput, + DatePickerValue, + isValidDate, +} from '@ballerine/ui'; +import { FunctionComponent, useCallback } from 'react'; + +export interface IDateFieldOptions { + disableFuture?: boolean; + disablePast?: boolean; + outputFormat?: 'date' | 'iso'; +} + +const defaultOptions: IDateFieldOptions = { + disableFuture: false, + disablePast: false, + outputFormat: undefined, +}; + +export const DateField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options = defaultOptions, stack }) => { + const { onBlur, onChange, value, disabled } = fieldProps; + const { + disableFuture = defaultOptions.disableFuture, + disablePast = defaultOptions.disablePast, + outputFormat = defaultOptions.outputFormat, + } = options; + + const handleChange = useCallback( + (event: DatePickerChangeEvent) => { + const dateValue = event.target.value; + + if (dateValue === null) return onChange(null); + + if (!isValidDate(dateValue)) return; + + onChange(dateValue); + }, + [onChange], + ); + + return ( + + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/index.ts new file mode 100644 index 0000000000..0e91777402 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/index.ts @@ -0,0 +1 @@ +export * from './DateField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/DocumentField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/DocumentField.tsx new file mode 100644 index 0000000000..cb066b3958 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/DocumentField.tsx @@ -0,0 +1,92 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { FileUploaderField } from '@/components/organisms/UIRenderer/elements/JSONForm/components/FileUploaderField'; +import { useFileRepository } from '@/components/organisms/UIRenderer/elements/JSONForm/components/FileUploaderField/hooks/useFileRepository'; +import { useValidator } from '@/components/providers/Validator/hooks/useValidator'; +import { Document } from '@/domains/collection-flow'; +import { fetchFile } from '@/domains/storage/storage.api'; +import { collectionFlowFileStorage } from '@/pages/CollectionFlow/collection-flow.file-storage'; +import { FieldErrors } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { useDocument } from '@/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocument'; +import { useDocumentUpload } from '@/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocumentUpload'; +import { useDocuments } from '@/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocuments'; +import { useEventEmmitter } from '@/pages/CollectionFlowV2/hocs/withConnectedField'; +import { useUIElement } from '@/pages/CollectionFlowV2/hooks/useUIElement'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { ErrorsList } from '@ballerine/ui'; +import { FunctionComponent, useCallback, useLayoutEffect } from 'react'; + +export type TDocumentFieldValueType = string | undefined; +export interface IDocumentFieldParams { + initialData: Partial; + pageIndex?: number; +} + +export const DocumentField: FunctionComponent< + IFieldComponentProps +> = ({ definition, stack, options, fieldProps }) => { + const { payload } = useStateManagerContext(); + const uiElement = useUIElement(definition, payload, stack); + console.log('doc id', uiElement.getId()); + const documents = useDocuments(uiElement); + const { fileId, setDocument, clearDocument } = useDocument({ + documents, + params: options, + uiElement, + }); + const { fileUploader, isUploading, uploadError } = useDocumentUpload(); + const { validate } = useValidator(); + + const emitEvent = useEventEmmitter(uiElement); + + const { onBlur, disabled } = fieldProps; + useFileRepository(collectionFlowFileStorage, fileId); + + useLayoutEffect(() => { + if (!fileId) return; + + const persistedFile = collectionFlowFileStorage.getFileById(fileId); + + if (persistedFile) return; + + void fetchFile(fileId).then(file => { + const createdFile = new File([''], file.fileNameInBucket || file.fileNameOnDisk || '', { + type: 'text/plain', + }); + + collectionFlowFileStorage.registerFile(fileId, createdFile); + }); + }, [fileId]); + + const handleChange = useCallback( + (fileId: string) => { + setDocument(fileId); + emitEvent('onChange'); + validate(); + }, + [setDocument, emitEvent, validate], + ); + + return ( + + void} + testId={uiElement.getId()} + onChange={handleChange} + /> + {/* {!!warnings.length && err.message)} />} + {isTouched && !!validationErrors.length && ( + error.message)} /> + )} */} + {uploadError && } + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocument/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocument/index.ts new file mode 100644 index 0000000000..75d28b795e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocument/index.ts @@ -0,0 +1 @@ +export * from './useDocument'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocument/useDocument.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocument/useDocument.ts new file mode 100644 index 0000000000..9033fa6400 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocument/useDocument.ts @@ -0,0 +1,98 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; +import { Document } from '@/domains/collection-flow'; +import { IDocumentFieldParams } from '@/pages/CollectionFlowV2/components/ui/fields/DocumentField/DocumentField'; +import remove from 'lodash/remove'; +import set from 'lodash/set'; +import { useCallback, useMemo } from 'react'; + +interface IUseDocumentParams { + documents: Document[]; + params: IDocumentFieldParams; + uiElement: UIElement; +} + +export const useDocument = ({ documents, params, uiElement }: IUseDocumentParams) => { + const { stateApi } = useStateManagerContext(); + + const document = useMemo(() => { + return documents?.find(document => document.id === params.initialData.id); + }, [documents, params]); + + const setDocument = useCallback( + (fileId: string) => { + const { initialData, pageIndex = 0 } = params; + + if (!document) { + const newDocument = structuredClone(initialData); + if (!newDocument.pages) { + newDocument.pages = []; + } + + newDocument.pages[pageIndex] = { ballerineFileId: fileId }; + + const newDocuments = [...documents, newDocument]; + + const context = stateApi.getContext(); + set(context, uiElement.getValueDestination(), newDocuments); + + stateApi.setContext(context); + } else { + document.pages![pageIndex] = { ballerineFileId: fileId }; + + const newDocuments = documents.map(document => { + if (document.id === params.initialData.id) { + return { ...document }; + } + + return document; + }); + + const context = stateApi.getContext(); + set(context, uiElement.getValueDestination(), newDocuments); + + stateApi.setContext(context); + } + }, + [params, uiElement, document, documents, stateApi], + ); + + const clearDocument = useCallback(() => { + if (!document) return; + + const newDocument = structuredClone(document); + const context = stateApi.getContext(); + + // removing document page at pageIndex + remove(newDocument?.pages || [], (page, index) => index === (params.pageIndex || 0)); + + // if no pages left, remove the document from documents array + if (!newDocument?.pages?.length) { + const newDocuments = remove(documents, document => document.id === newDocument.id); + + set(context, uiElement.getValueDestination(), newDocuments); + + stateApi.setContext(context); + } else { + set( + context, + uiElement.getValueDestination(), + documents.map(document => { + if (document.id === newDocument.id) { + return newDocument; + } + + return document; + }), + ); + + stateApi.setContext(context); + } + }, [params, document, documents, uiElement, stateApi]); + + const fileId = useMemo(() => { + return document?.pages?.[params.pageIndex || 0]?.ballerineFileId; + }, [document, params]); + + return { fileId, setDocument, clearDocument }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/index.ts new file mode 100644 index 0000000000..b067342cd0 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/index.ts @@ -0,0 +1 @@ +export * from './useDocumentUpload'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts new file mode 100644 index 0000000000..a847b8f468 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts @@ -0,0 +1,46 @@ +import { UploadFileFn } from '@/components/organisms/UIRenderer/elements/JSONForm/components/FileUploaderField/hooks/useFileUploading/types'; +import { uploadFile } from '@/domains/storage/storage.api'; +import { HTTPError } from 'ky'; +import { useCallback, useState } from 'react'; + +interface IUseDocumentUploadState { + uploadError: Error | null; + isUploading: boolean; +} + +export const useDocumentUpload = () => { + const [state, setState] = useState({ + uploadError: null, + isUploading: false, + }); + + const fileUploader: UploadFileFn = useCallback(async (file: File) => { + setState(prevState => ({ ...prevState, isUploading: true })); + + try { + const { id: fileId } = await uploadFile({ file }); + + setState(prevState => ({ ...prevState, uploadError: null })); + + return { fileId }; + } catch (error) { + if (error instanceof HTTPError) { + console.error('Error uploading file', error); + setState(prevState => ({ ...prevState, uploadError: error })); + + throw error; + } + + setState(prevState => ({ ...prevState, uploadError: new Error('Failed to upload file.') })); + + throw error; + } finally { + setState(prevState => ({ ...prevState, isUploading: false })); + } + }, []); + + return { + ...state, + fileUploader, + }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocuments/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocuments/index.ts new file mode 100644 index 0000000000..f6ff11ffd5 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocuments/index.ts @@ -0,0 +1 @@ +export * from './useDocuments'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocuments/useDocuments.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocuments/useDocuments.ts new file mode 100644 index 0000000000..f5ad224769 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/hooks/useDocuments/useDocuments.ts @@ -0,0 +1,10 @@ +import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; +import { useMemo } from 'react'; + +export const useDocuments = (uiElement: UIElement) => { + const documents = useMemo(() => { + return uiElement.getValue() || []; + }, [uiElement]); + + return documents; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/index.ts new file mode 100644 index 0000000000..4aa75d4e29 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DocumentField/index.ts @@ -0,0 +1 @@ +export * from './DocumentField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/IndustriesField/IndustriesField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/IndustriesField/IndustriesField.tsx new file mode 100644 index 0000000000..9a7f556ebe --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/IndustriesField/IndustriesField.tsx @@ -0,0 +1,45 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import { FunctionComponent, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface IIndustriesFieldOptions { + placeholder?: string; +} + +export const IndustriesField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options, stack }) => { + const { placeholder } = options; + const { value, disabled, onChange, onBlur } = fieldProps; + + const { t } = useTranslation(); + + const translatedIndustries = t('industries', { returnObjects: true }) as string[]; + + const dropdownOptions = useMemo( + () => + translatedIndustries.map(industry => ({ + label: industry, + value: industry, + })), + [translatedIndustries], + ); + + return ( + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/IndustriesField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/IndustriesField/index.ts new file mode 100644 index 0000000000..9d8a407ae9 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/IndustriesField/index.ts @@ -0,0 +1 @@ +export * from './IndustriesField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/LocaleField/LocaleField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/LocaleField/LocaleField.tsx new file mode 100644 index 0000000000..f43f80e68d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/LocaleField/LocaleField.tsx @@ -0,0 +1,51 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import { FunctionComponent, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface ILocaleFieldOptions { + placeholder?: string; +} + +export const LocaleField: FunctionComponent> = ({ + fieldProps, + definition, + options, + stack, +}) => { + const { value, disabled, onChange, onBlur } = fieldProps; + const { placeholder = '' } = options || {}; + + const { t } = useTranslation(); + + const dropdownOptions = useMemo( + () => + ( + t('languages', { returnObjects: true }) as Array<{ + const: string; + title: string; + }> + ).map(({ const: constValue, title }) => ({ + label: title, + value: constValue, + })), + [t], + ); + + return ( + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/LocaleField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/LocaleField/index.ts new file mode 100644 index 0000000000..4a2e5cb805 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/LocaleField/index.ts @@ -0,0 +1 @@ +export * from './LocaleField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/MCCField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/MCCField.tsx new file mode 100644 index 0000000000..34dbe397a4 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/MCCField.tsx @@ -0,0 +1,35 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { mccOptions } from '@/pages/CollectionFlowV2/components/ui/fields/MCCField/options'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export interface IMCCFieldOptions { + placeholder?: string; +} + +export const MCCField: FunctionComponent> = ({ + fieldProps, + definition, + options, + stack, +}) => { + const { placeholder } = options; + const { value, disabled, onChange, onBlur } = fieldProps; + + return ( + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/index.ts new file mode 100644 index 0000000000..017388ab08 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/index.ts @@ -0,0 +1 @@ +export * from './MCCField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/options.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/options.ts new file mode 100644 index 0000000000..7e443150e8 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MCCField/options.ts @@ -0,0 +1,3941 @@ +export const mccOptions = [ + { + mcc: '0742', + description: 'Veterinary Services', + }, + { + mcc: '0763', + description: 'Agricultural Co-operatives', + }, + { + mcc: '0780', + description: 'Horticultural Services, Landscaping Services', + }, + { + mcc: '1520', + description: 'General Contractors-Residential and Commercial', + }, + { + mcc: '1711', + description: + 'Air Conditioning Contractors – Sales and Installation, Heating Contractors – Sales, Service, Installation', + }, + { + mcc: '1731', + description: 'Electrical Contractors', + }, + { + mcc: '1740', + description: + 'Insulation – Contractors, Masonry, Stonework Contractors, Plastering Contractors, Stonework and Masonry Contractors, Tile Settings Contractors', + }, + { + mcc: '1750', + description: 'Carpentry Contractors', + }, + { + mcc: '1761', + description: 'Roofing – Contractors, Sheet Metal Work – Contractors, Siding – Contractors', + }, + { + mcc: '1771', + description: 'Contractors – Concrete Work', + }, + { + mcc: '1799', + description: 'Contractors – Special Trade, Not Elsewhere Classified', + }, + { + mcc: '2741', + description: 'Miscellaneous Publishing and Printing', + }, + { + mcc: '2791', + description: 'Typesetting, Plate Making, & Related Services', + }, + { + mcc: '2842', + description: 'Specialty Cleaning, Polishing, and Sanitation Preparations', + }, + { + mcc: '3000', + description: 'UNITED AIRLINES', + }, + { + mcc: '3001', + description: 'AMERICAN AIRLINES', + }, + { + mcc: '3002', + description: 'PAN AMERICAN', + }, + { + mcc: '3003', + description: 'Airlines', + }, + { + mcc: '3004', + description: 'TRANS WORLD AIRLINES', + }, + { + mcc: '3005', + description: 'BRITISH AIRWAYS', + }, + { + mcc: '3006', + description: 'JAPAN AIRLINES', + }, + { + mcc: '3007', + description: 'AIR FRANCE', + }, + { + mcc: '3008', + description: 'LUFTHANSA', + }, + { + mcc: '3009', + description: 'AIR CANADA', + }, + { + mcc: '3010', + description: 'KLM (ROYAL DUTCH AIRLINES)', + }, + { + mcc: '3011', + description: 'AEORFLOT', + }, + { + mcc: '3012', + description: 'QANTAS', + }, + { + mcc: '3013', + description: 'ALITALIA', + }, + { + mcc: '3014', + description: 'SAUDIA ARABIAN AIRLINES', + }, + { + mcc: '3015', + description: 'SWISSAIR', + }, + { + mcc: '3016', + description: 'SAS', + }, + { + mcc: '3017', + description: 'SOUTH AFRICAN AIRWAYS', + }, + { + mcc: '3018', + description: 'VARIG (BRAZIL)', + }, + { + mcc: '3019', + description: 'Airlines', + }, + { + mcc: '3020', + description: 'AIR-INDIA', + }, + { + mcc: '3021', + description: 'AIR ALGERIE', + }, + { + mcc: '3022', + description: 'PHILIPPINE AIRLINES', + }, + { + mcc: '3023', + description: 'MEXICANA', + }, + { + mcc: '3024', + description: 'PAKISTAN INTERNATIONAL', + }, + { + mcc: '3025', + description: 'AIR NEW ZEALAND', + }, + { + mcc: '3026', + description: 'Airlines', + }, + { + mcc: '3027', + description: 'UTA/INTERAIR', + }, + { + mcc: '3028', + description: 'AIR MALTA', + }, + { + mcc: '3029', + description: 'SABENA', + }, + { + mcc: '3030', + description: 'AEROLINEAS ARGENTINAS', + }, + { + mcc: '3031', + description: 'OLYMPIC AIRWAYS', + }, + { + mcc: '3032', + description: 'EL AL', + }, + { + mcc: '3033', + description: 'ANSETT AIRLINES', + }, + { + mcc: '3034', + description: 'AUSTRAINLIAN AIRLINES', + }, + { + mcc: '3035', + description: 'TAP (PORTUGAL)', + }, + { + mcc: '3036', + description: 'VASP (BRAZIL)', + }, + { + mcc: '3037', + description: 'EGYPTAIR', + }, + { + mcc: '3038', + description: 'KUWAIT AIRLINES', + }, + { + mcc: '3039', + description: 'AVIANCA', + }, + { + mcc: '3040', + description: 'GULF AIR (BAHRAIN)', + }, + { + mcc: '3041', + description: 'BALKAN-BULGARIAN AIRLINES', + }, + { + mcc: '3042', + description: 'FINNAIR', + }, + { + mcc: '3043', + description: 'AER LINGUS', + }, + { + mcc: '3044', + description: 'AIR LANKA', + }, + { + mcc: '3045', + description: 'NIGERIA AIRWAYS', + }, + { + mcc: '3046', + description: 'CRUZEIRO DO SUL (BRAZIJ)', + }, + { + mcc: '3047', + description: 'THY (TURKEY)', + }, + { + mcc: '3048', + description: 'ROYAL AIR MAROC', + }, + { + mcc: '3049', + description: 'TUNIS AIR', + }, + { + mcc: '3050', + description: 'ICELANDAIR', + }, + { + mcc: '3051', + description: 'AUSTRIAN AIRLINES', + }, + { + mcc: '3052', + description: 'LANCHILE', + }, + { + mcc: '3053', + description: 'AVIACO (SPAIN)', + }, + { + mcc: '3054', + description: 'LADECO (CHILE)', + }, + { + mcc: '3055', + description: 'LAB (BOLIVIA)', + }, + { + mcc: '3056', + description: 'QUEBECAIRE', + }, + { + mcc: '3057', + description: 'EASTWEST AIRLINES (AUSTRALIA)', + }, + { + mcc: '3058', + description: 'DELTA', + }, + { + mcc: '3059', + description: 'Airlines', + }, + { + mcc: '3060', + description: 'NORTHWEST', + }, + { + mcc: '3061', + description: 'CONTINENTAL', + }, + { + mcc: '3062', + description: 'WESTERN', + }, + { + mcc: '3063', + description: 'US AIR', + }, + { + mcc: '3064', + description: 'Airlines', + }, + { + mcc: '3065', + description: 'AIRINTER', + }, + { + mcc: '3066', + description: 'SOUTHWEST', + }, + { + mcc: '3067', + description: 'Airlines', + }, + { + mcc: '3068', + description: 'Airlines', + }, + { + mcc: '3069', + description: 'SUN COUNTRY AIRLINES', + }, + { + mcc: '3070', + description: 'Airlines', + }, + { + mcc: '3071', + description: 'AIR BRITISH COLUBIA', + }, + { + mcc: '3072', + description: 'Airlines', + }, + { + mcc: '3073', + description: 'Airlines', + }, + { + mcc: '3074', + description: 'Airlines', + }, + { + mcc: '3075', + description: 'SINGAPORE AIRLINES', + }, + { + mcc: '3076', + description: 'AEROMEXICO', + }, + { + mcc: '3077', + description: 'THAI AIRWAYS', + }, + { + mcc: '3078', + description: 'CHINA AIRLINES', + }, + { + mcc: '3079', + description: 'Airlines', + }, + { + mcc: '3080', + description: 'Airlines', + }, + { + mcc: '3081', + description: 'NORDAIR', + }, + { + mcc: '3082', + description: 'KOREAN AIRLINES', + }, + { + mcc: '3083', + description: 'AIR AFRIGUE', + }, + { + mcc: '3084', + description: 'EVA AIRLINES', + }, + { + mcc: '3085', + description: 'MIDWEST EXPRESS AIRLINES, INC.', + }, + { + mcc: '3086', + description: 'Airlines', + }, + { + mcc: '3087', + description: 'METRO AIRLINES', + }, + { + mcc: '3088', + description: 'CROATIA AIRLINES', + }, + { + mcc: '3089', + description: 'TRANSAERO', + }, + { + mcc: '3090', + description: 'Airlines', + }, + { + mcc: '3091', + description: 'Airlines', + }, + { + mcc: '3092', + description: 'Airlines', + }, + { + mcc: '3093', + description: 'Airlines', + }, + { + mcc: '3094', + description: 'ZAMBIA AIRWAYS', + }, + { + mcc: '3095', + description: 'Airlines', + }, + { + mcc: '3096', + description: 'AIR ZIMBABWE', + }, + { + mcc: '3097', + description: 'Airlines', + }, + { + mcc: '3098', + description: 'Airlines', + }, + { + mcc: '3099', + description: 'CATHAY PACIFIC', + }, + { + mcc: '3100', + description: 'MALAYSIAN AIRLINE SYSTEM', + }, + { + mcc: '3101', + description: 'Airlines', + }, + { + mcc: '3102', + description: 'IBERIA', + }, + { + mcc: '3103', + description: 'GARUDA (INDONESIA)', + }, + { + mcc: '3104', + description: 'Airlines', + }, + { + mcc: '3105', + description: 'Airlines', + }, + { + mcc: '3106', + description: 'BRAATHENS S.A.F.E. (NORWAY)', + }, + { + mcc: '3107', + description: 'Airlines', + }, + { + mcc: '3108', + description: 'Airlines', + }, + { + mcc: '3109', + description: 'Airlines', + }, + { + mcc: '3110', + description: 'WINGS AIRWAYS', + }, + { + mcc: '3111', + description: 'BRITISH MIDLAND', + }, + { + mcc: '3112', + description: 'WINDWARD ISLAND', + }, + { + mcc: '3113', + description: 'Airlines', + }, + { + mcc: '3114', + description: 'Airlines', + }, + { + mcc: '3115', + description: 'Airlines', + }, + { + mcc: '3116', + description: 'Airlines', + }, + { + mcc: '3117', + description: 'VIASA', + }, + { + mcc: '3118', + description: 'VALLEY AIRLINES', + }, + { + mcc: '3119', + description: 'Airlines', + }, + { + mcc: '3120', + description: 'Airlines', + }, + { + mcc: '3121', + description: 'Airlines', + }, + { + mcc: '3122', + description: 'Airlines', + }, + { + mcc: '3123', + description: 'Airlines', + }, + { + mcc: '3124', + description: 'Airlines', + }, + { + mcc: '3125', + description: 'TAN', + }, + { + mcc: '3126', + description: 'TALAIR', + }, + { + mcc: '3127', + description: 'TACA INTERNATIONAL', + }, + { + mcc: '3128', + description: 'Airlines', + }, + { + mcc: '3129', + description: 'SURINAM AIRWAYS', + }, + { + mcc: '3130', + description: 'SUN WORLD INTERNATIONAL', + }, + { + mcc: '3131', + description: 'Airlines', + }, + { + mcc: '3132', + description: 'Airlines', + }, + { + mcc: '3133', + description: 'SUNBELT AIRLINES', + }, + { + mcc: '3134', + description: 'Airlines', + }, + { + mcc: '3135', + description: 'SUDAN AIRWAYS', + }, + { + mcc: '3136', + description: 'Airlines', + }, + { + mcc: '3137', + description: 'SINGLETON', + }, + { + mcc: '3138', + description: 'SIMMONS AIRLINES', + }, + { + mcc: '3139', + description: 'Airlines', + }, + { + mcc: '3140', + description: 'Airlines', + }, + { + mcc: '3141', + description: 'Airlines', + }, + { + mcc: '3142', + description: 'Airlines', + }, + { + mcc: '3143', + description: 'SCENIC AIRLINES', + }, + { + mcc: '3144', + description: 'VIRGIN ATLANTIC', + }, + { + mcc: '3145', + description: 'SAN JUAN AIRLINES', + }, + { + mcc: '3146', + description: 'LUXAIR', + }, + { + mcc: '3147', + description: 'Airlines', + }, + { + mcc: '3148', + description: 'Airlines', + }, + { + mcc: '3149', + description: 'Airlines', + }, + { + mcc: '3150', + description: 'Airlines', + }, + { + mcc: '3151', + description: 'AIR ZAIRE', + }, + { + mcc: '3152', + description: 'Airlines', + }, + { + mcc: '3153', + description: 'Airlines', + }, + { + mcc: '3154', + description: 'PRINCEVILLE', + }, + { + mcc: '3155', + description: 'Airlines', + }, + { + mcc: '3156', + description: 'Airlines', + }, + { + mcc: '3157', + description: 'Airlines', + }, + { + mcc: '3158', + description: 'Airlines', + }, + { + mcc: '3159', + description: 'PBA', + }, + { + mcc: '3160', + description: 'Airlines', + }, + { + mcc: '3161', + description: 'ALL NIPPON AIRWAYS', + }, + { + mcc: '3162', + description: 'Airlines', + }, + { + mcc: '3163', + description: 'Airlines', + }, + { + mcc: '3164', + description: 'NORONTAIR', + }, + { + mcc: '3165', + description: 'NEW YORK HELICOPTER', + }, + { + mcc: '3166', + description: 'Airlines', + }, + { + mcc: '3167', + description: 'Airlines', + }, + { + mcc: '3168', + description: 'Airlines', + }, + { + mcc: '3169', + description: 'Airlines', + }, + { + mcc: '3170', + description: 'NOUNT COOK', + }, + { + mcc: '3171', + description: 'CANADIAN AIRLINES INTERNATIONAL', + }, + { + mcc: '3172', + description: 'NATIONAIR', + }, + { + mcc: '3173', + description: 'Airlines', + }, + { + mcc: '3174', + description: 'Airlines', + }, + { + mcc: '3175', + description: 'Airlines', + }, + { + mcc: '3176', + description: 'METROFLIGHT AIRLINES', + }, + { + mcc: '3177', + description: 'Airlines', + }, + { + mcc: '3178', + description: 'MESA AIR', + }, + { + mcc: '3179', + description: 'Airlines', + }, + { + mcc: '3180', + description: 'Airlines', + }, + { + mcc: '3181', + description: 'MALEV', + }, + { + mcc: '3182', + description: 'LOT (POLAND)', + }, + { + mcc: '3183', + description: 'Airlines', + }, + { + mcc: '3184', + description: 'LIAT', + }, + { + mcc: '3185', + description: 'LAV (VENEZUELA)', + }, + { + mcc: '3186', + description: 'LAP (PARAGUAY)', + }, + { + mcc: '3187', + description: 'LACSA (COSTA RICA)', + }, + { + mcc: '3188', + description: 'Airlines', + }, + { + mcc: '3189', + description: 'Airlines', + }, + { + mcc: '3190', + description: 'JUGOSLAV AIR', + }, + { + mcc: '3191', + description: 'ISLAND AIRLINES', + }, + { + mcc: '3192', + description: 'IRAN AIR', + }, + { + mcc: '3193', + description: 'INDIAN AIRLINES', + }, + { + mcc: '3194', + description: 'Airlines', + }, + { + mcc: '3195', + description: 'Airlines', + }, + { + mcc: '3196', + description: 'HAWAIIAN AIR', + }, + { + mcc: '3197', + description: 'HAVASU AIRLINES', + }, + { + mcc: '3198', + description: 'Airlines', + }, + { + mcc: '3199', + description: 'Airlines', + }, + { + mcc: '3200', + description: 'FUYANA AIRWAYS', + }, + { + mcc: '3201', + description: 'Airlines', + }, + { + mcc: '3202', + description: 'Airlines', + }, + { + mcc: '3203', + description: 'GOLDEN PACIFIC AIR', + }, + { + mcc: '3204', + description: 'FREEDOM AIR', + }, + { + mcc: '3205', + description: 'Airlines', + }, + { + mcc: '3206', + description: 'Airlines', + }, + { + mcc: '3207', + description: 'Airlines', + }, + { + mcc: '3208', + description: 'Airlines', + }, + { + mcc: '3209', + description: 'Airlines', + }, + { + mcc: '3210', + description: 'Airlines', + }, + { + mcc: '3211', + description: 'Airlines', + }, + { + mcc: '3212', + description: 'DOMINICANA', + }, + { + mcc: '3213', + description: 'Airlines', + }, + { + mcc: '3214', + description: 'Airlines', + }, + { + mcc: '3215', + description: 'DAN AIR SERVICES', + }, + { + mcc: '3216', + description: 'CUMBERLAND AIRLINES', + }, + { + mcc: '3217', + description: 'CSA', + }, + { + mcc: '3218', + description: 'CROWN AIR', + }, + { + mcc: '3219', + description: 'COPA', + }, + { + mcc: '3220', + description: 'COMPANIA FAUCETT', + }, + { + mcc: '3221', + description: 'TRANSPORTES AEROS MILITARES ECCUATORANOS', + }, + { + mcc: '3222', + description: 'COMMAND AIRWAYS', + }, + { + mcc: '3223', + description: 'COMAIR', + }, + { + mcc: '3224', + description: 'Airlines', + }, + { + mcc: '3225', + description: 'Airlines', + }, + { + mcc: '3226', + description: 'Airlines', + }, + { + mcc: '3227', + description: 'Airlines', + }, + { + mcc: '3228', + description: 'CAYMAN AIRWAYS', + }, + { + mcc: '3229', + description: 'SAETA SOCIAEDAD ECUATORIANOS DE TRANSPORTES AEREOS', + }, + { + mcc: '3230', + description: 'Airlines', + }, + { + mcc: '3231', + description: 'SASHA SERVICIO AERO DE HONDURAS', + }, + { + mcc: '3232', + description: 'Airlines', + }, + { + mcc: '3233', + description: 'CAPITOL AIR', + }, + { + mcc: '3234', + description: 'BWIA', + }, + { + mcc: '3235', + description: 'BROKWAY AIR', + }, + { + mcc: '3236', + description: 'Airlines', + }, + { + mcc: '3237', + description: 'Airlines', + }, + { + mcc: '3238', + description: 'BEMIDJI AIRLINES', + }, + { + mcc: '3239', + description: 'BAR HARBOR AIRLINES', + }, + { + mcc: '3240', + description: 'BAHAMASAIR', + }, + { + mcc: '3241', + description: 'AVIATECA (GUATEMALA)', + }, + { + mcc: '3242', + description: 'AVENSA', + }, + { + mcc: '3243', + description: 'AUSTRIAN AIR SERVICE', + }, + { + mcc: '3244', + description: 'Airlines', + }, + { + mcc: '3245', + description: 'Airlines', + }, + { + mcc: '3246', + description: 'Airlines', + }, + { + mcc: '3247', + description: 'Airlines', + }, + { + mcc: '3248', + description: 'Airlines', + }, + { + mcc: '3249', + description: 'Airlines', + }, + { + mcc: '3250', + description: 'Airlines', + }, + { + mcc: '3251', + description: 'ALOHA AIRLINES', + }, + { + mcc: '3252', + description: 'ALM', + }, + { + mcc: '3253', + description: 'AMERICA WEST', + }, + { + mcc: '3254', + description: 'TRUMP AIRLINE', + }, + { + mcc: '3255', + description: 'Airlines', + }, + { + mcc: '3256', + description: 'ALASKA AIRLINES', + }, + { + mcc: '3257', + description: 'Airlines', + }, + { + mcc: '3258', + description: 'Airlines', + }, + { + mcc: '3259', + description: 'AMERICAN TRANS AIR', + }, + { + mcc: '3260', + description: 'Airlines', + }, + { + mcc: '3261', + description: 'AIR CHINA', + }, + { + mcc: '3262', + description: 'RENO AIR, INC.', + }, + { + mcc: '3263', + description: 'Airlines', + }, + { + mcc: '3264', + description: 'Airlines', + }, + { + mcc: '3265', + description: 'Airlines', + }, + { + mcc: '3266', + description: 'AIR SEYCHELLES', + }, + { + mcc: '3267', + description: 'AIR PANAMA', + }, + { + mcc: '3268', + description: 'Airlines', + }, + { + mcc: '3269', + description: 'Airlines', + }, + { + mcc: '3270', + description: 'Airlines', + }, + { + mcc: '3271', + description: 'Airlines', + }, + { + mcc: '3272', + description: 'Airlines', + }, + { + mcc: '3273', + description: 'Airlines', + }, + { + mcc: '3274', + description: 'Airlines', + }, + { + mcc: '3275', + description: 'Airlines', + }, + { + mcc: '3276', + description: 'Airlines', + }, + { + mcc: '3277', + description: 'Airlines', + }, + { + mcc: '3278', + description: 'Airlines', + }, + { + mcc: '3279', + description: 'Airlines', + }, + { + mcc: '3280', + description: 'AIR JAMAICA', + }, + { + mcc: '3281', + description: 'Airlines', + }, + { + mcc: '3282', + description: 'AIR DJIBOUTI', + }, + { + mcc: '3283', + description: 'Airlines', + }, + { + mcc: '3284', + description: 'AERO VIRGIN ISLANDS', + }, + { + mcc: '3285', + description: 'AERO PERU', + }, + { + mcc: '3286', + description: 'AEROLINEAS NICARAGUENSIS', + }, + { + mcc: '3287', + description: 'AERO COACH AVAIATION', + }, + { + mcc: '3288', + description: 'Airlines', + }, + { + mcc: '3289', + description: 'Airlines', + }, + { + mcc: '3290', + description: 'Airlines', + }, + { + mcc: '3291', + description: 'ARIANA AFGHAN', + }, + { + mcc: '3292', + description: 'CYPRUS AIRWAYS', + }, + { + mcc: '3293', + description: 'ECUATORIANA', + }, + { + mcc: '3294', + description: 'ETHIOPIAN AIRLINES', + }, + { + mcc: '3295', + description: 'KENYA AIRLINES', + }, + { + mcc: '3296', + description: 'Airlines', + }, + { + mcc: '3297', + description: 'Airlines', + }, + { + mcc: '3298', + description: 'AIR MAURITIUS', + }, + { + mcc: '3299', + description: 'WIDERO’S FLYVESELSKAP', + }, + { + mcc: '3351', + description: 'AFFILIATED AUTO RENTAL', + }, + { + mcc: '3352', + description: 'AMERICAN INTL RENT-A-CAR', + }, + { + mcc: '3353', + description: 'BROOKS RENT-A-CAR', + }, + { + mcc: '3354', + description: 'ACTION AUTO RENTAL', + }, + { + mcc: '3355', + description: 'Car Rental', + }, + { + mcc: '3356', + description: 'Car Rental', + }, + { + mcc: '3357', + description: 'HERTZ RENT-A-CAR', + }, + { + mcc: '3358', + description: 'Car Rental', + }, + { + mcc: '3359', + description: 'PAYLESS CAR RENTAL', + }, + { + mcc: '3360', + description: 'SNAPPY CAR RENTAL', + }, + { + mcc: '3361', + description: 'AIRWAYS RENT-A-CAR', + }, + { + mcc: '3362', + description: 'ALTRA AUTO RENTAL', + }, + { + mcc: '3363', + description: 'Car Rental', + }, + { + mcc: '3364', + description: 'AGENCY RENT-A-CAR', + }, + { + mcc: '3365', + description: 'Car Rental', + }, + { + mcc: '3366', + description: 'BUDGET RENT-A-CAR', + }, + { + mcc: '3367', + description: 'Car Rental', + }, + { + mcc: '3368', + description: 'HOLIDAY RENT-A-WRECK', + }, + { + mcc: '3369', + description: 'Car Rental', + }, + { + mcc: '3370', + description: 'RENT-A-WRECK', + }, + { + mcc: '3371', + description: 'Car Rental', + }, + { + mcc: '3372', + description: 'Car Rental', + }, + { + mcc: '3373', + description: 'Car Rental', + }, + { + mcc: '3374', + description: 'Car Rental', + }, + { + mcc: '3375', + description: 'Car Rental', + }, + { + mcc: '3376', + description: 'AJAX RENT-A-CAR', + }, + { + mcc: '3377', + description: 'Car Rental', + }, + { + mcc: '3378', + description: 'Car Rental', + }, + { + mcc: '3379', + description: 'Car Rental', + }, + { + mcc: '3380', + description: 'Car Rental', + }, + { + mcc: '3381', + description: 'EUROP CAR', + }, + { + mcc: '3382', + description: 'Car Rental', + }, + { + mcc: '3383', + description: 'Car Rental', + }, + { + mcc: '3384', + description: 'Car Rental', + }, + { + mcc: '3385', + description: 'TROPICAL RENT-A-CAR', + }, + { + mcc: '3386', + description: 'SHOWCASE RENTAL CARS', + }, + { + mcc: '3387', + description: 'ALAMO RENT-A-CAR', + }, + { + mcc: '3388', + description: 'Car Rental', + }, + { + mcc: '3389', + description: 'AVIS RENT-A-CAR', + }, + { + mcc: '3390', + description: 'DOLLAR RENT-A-CAR', + }, + { + mcc: '3391', + description: 'EUROPE BY CAR', + }, + { + mcc: '3392', + description: 'Car Rental', + }, + { + mcc: '3393', + description: 'NATIONAL CAR RENTAL', + }, + { + mcc: '3394', + description: 'KEMWELL GROUP RENT-A-CAR', + }, + { + mcc: '3395', + description: 'THRIFTY RENT-A-CAR', + }, + { + mcc: '3396', + description: 'TILDEN TENT-A-CAR', + }, + { + mcc: '3397', + description: 'Car Rental', + }, + { + mcc: '3398', + description: 'ECONO-CAR RENT-A-CAR', + }, + { + mcc: '3399', + description: 'Car Rental', + }, + { + mcc: '3400', + description: 'AUTO HOST COST CAR RENTALS', + }, + { + mcc: '3401', + description: 'Car Rental', + }, + { + mcc: '3402', + description: 'Car Rental', + }, + { + mcc: '3403', + description: 'Car Rental', + }, + { + mcc: '3404', + description: 'Car Rental', + }, + { + mcc: '3405', + description: 'ENTERPRISE RENT-A-CAR', + }, + { + mcc: '3406', + description: 'Car Rental', + }, + { + mcc: '3407', + description: 'Car Rental', + }, + { + mcc: '3408', + description: 'Car Rental', + }, + { + mcc: '3409', + description: 'GENERAL RENT-A-CAR', + }, + { + mcc: '3410', + description: 'Car Rental', + }, + { + mcc: '3411', + description: 'Car Rental', + }, + { + mcc: '3412', + description: 'A-1 RENT-A-CAR', + }, + { + mcc: '3413', + description: 'Car Rental', + }, + { + mcc: '3414', + description: 'GODFREY NATL RENT-A-CAR', + }, + { + mcc: '3415', + description: 'Car Rental', + }, + { + mcc: '3416', + description: 'Car Rental', + }, + { + mcc: '3417', + description: 'Car Rental', + }, + { + mcc: '3418', + description: 'Car Rental', + }, + { + mcc: '3419', + description: 'ALPHA RENT-A-CAR', + }, + { + mcc: '3420', + description: 'ANSA INTL RENT-A-CAR', + }, + { + mcc: '3421', + description: 'ALLSTAE RENT-A-CAR', + }, + { + mcc: '3422', + description: 'Car Rental', + }, + { + mcc: '3423', + description: 'AVCAR RENT-A-CAR', + }, + { + mcc: '3424', + description: 'Car Rental', + }, + { + mcc: '3425', + description: 'AUTOMATE RENT-A-CAR', + }, + { + mcc: '3426', + description: 'Car Rental', + }, + { + mcc: '3427', + description: 'AVON RENT-A-CAR', + }, + { + mcc: '3428', + description: 'CAREY RENT-A-CAR', + }, + { + mcc: '3429', + description: 'INSURANCE RENT-A-CAR', + }, + { + mcc: '3430', + description: 'MAJOR RENT-A-CAR', + }, + { + mcc: '3431', + description: 'REPLACEMENT RENT-A-CAR', + }, + { + mcc: '3432', + description: 'RESERVE RENT-A-CAR', + }, + { + mcc: '3433', + description: 'UGLY DUCKLING RENT-A-CAR', + }, + { + mcc: '3434', + description: 'USA RENT-A-CAR', + }, + { + mcc: '3435', + description: 'VALUE RENT-A-CAR', + }, + { + mcc: '3436', + description: 'AUTOHANSA RENT-A-CAR', + }, + { + mcc: '3437', + description: 'CITE RENT-A-CAR', + }, + { + mcc: '3438', + description: 'INTERENT RENT-A-CAR', + }, + { + mcc: '3439', + description: 'MILLEVILLE RENT-A-CAR', + }, + { + mcc: '3440', + description: 'VIA ROUTE RENT-A-CAR', + }, + { + mcc: '3441', + description: 'Car Rental', + }, + { + mcc: '3501', + description: 'HOLIDAY INNS, HOLIDAY INN EXPRESS', + }, + { + mcc: '3502', + description: 'BEST WESTERN HOTELS', + }, + { + mcc: '3503', + description: 'SHERATON HOTELS', + }, + { + mcc: '3504', + description: 'HILTON HOTELS', + }, + { + mcc: '3505', + description: 'FORTE HOTELS', + }, + { + mcc: '3506', + description: 'GOLDEN TULIP HOTELS', + }, + { + mcc: '3507', + description: 'FRIENDSHIP INNS', + }, + { + mcc: '3508', + description: 'QUALITY INNS, QUALITY SUITES', + }, + { + mcc: '3509', + description: 'MARRIOTT HOTELS', + }, + { + mcc: '3510', + description: 'DAYS INN, DAYSTOP', + }, + { + mcc: '3511', + description: 'ARABELLA HOTELS', + }, + { + mcc: '3512', + description: 'INTER-CONTINENTAL HOTELS', + }, + { + mcc: '3513', + description: 'WESTIN HOTELS', + }, + { + mcc: '3514', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3515', + description: 'RODEWAY INNS', + }, + { + mcc: '3516', + description: 'LA QUINTA MOTOR INNS', + }, + { + mcc: '3517', + description: 'AMERICANA HOTELS', + }, + { + mcc: '3518', + description: 'SOL HOTELS', + }, + { + mcc: '3519', + description: 'PULLMAN INTERNATIONAL HOTELS', + }, + { + mcc: '3520', + description: 'MERIDIEN HOTELS', + }, + { + mcc: '3521', + description: 'CREST HOTELS (see FORTE HOTELS)', + }, + { + mcc: '3522', + description: 'TOKYO HOTEL', + }, + { + mcc: '3523', + description: 'PENNSULA HOTEL', + }, + { + mcc: '3524', + description: 'WELCOMGROUP HOTELS', + }, + { + mcc: '3525', + description: 'DUNFEY HOTELS', + }, + { + mcc: '3526', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3527', + description: 'DOWNTOWNER-PASSPORT HOTEL', + }, + { + mcc: '3528', + description: 'RED LION HOTELS, RED LION INNS', + }, + { + mcc: '3529', + description: 'CP HOTELS', + }, + { + mcc: '3530', + description: 'RENAISSANCE HOTELS, STOUFFER HOTELS', + }, + { + mcc: '3531', + description: 'ASTIR HOTELS', + }, + { + mcc: '3532', + description: 'SUN ROUTE HOTELS', + }, + { + mcc: '3533', + description: 'HOTEL IBIS', + }, + { + mcc: '3534', + description: 'SOUTHERN PACIFIC HOTELS', + }, + { + mcc: '3535', + description: 'HILTON INTERNATIONAL', + }, + { + mcc: '3536', + description: 'AMFAC HOTELS', + }, + { + mcc: '3537', + description: 'ANA HOTEL', + }, + { + mcc: '3538', + description: 'CONCORDE HOTELS', + }, + { + mcc: '3539', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3540', + description: 'IBEROTEL HOTELS', + }, + { + mcc: '3541', + description: 'HOTEL OKURA', + }, + { + mcc: '3542', + description: 'ROYAL HOTELS', + }, + { + mcc: '3543', + description: 'FOUR SEASONS HOTELS', + }, + { + mcc: '3544', + description: 'CIGA HOTELS', + }, + { + mcc: '3545', + description: 'SHANGRI-LA INTERNATIONAL', + }, + { + mcc: '3546', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3547', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3548', + description: 'HOTELES MELIA', + }, + { + mcc: '3549', + description: 'AUBERGE DES GOVERNEURS', + }, + { + mcc: '3550', + description: 'REGAL 8 INNS', + }, + { + mcc: '3551', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3552', + description: 'COAST HOTELS', + }, + { + mcc: '3553', + description: 'PARK INNS INTERNATIONAL', + }, + { + mcc: '3554', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3555', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3556', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3557', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3558', + description: 'JOLLY HOTELS', + }, + { + mcc: '3559', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3560', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3561', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3562', + description: 'COMFORT INNS', + }, + { + mcc: '3563', + description: 'JOURNEY’S END MOTLS', + }, + { + mcc: '3564', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3565', + description: 'RELAX INNS', + }, + { + mcc: '3566', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3567', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3568', + description: 'LADBROKE HOTELS', + }, + { + mcc: '3569', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3570', + description: 'FORUM HOTELS', + }, + { + mcc: '3571', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3572', + description: 'MIYAKO HOTELS', + }, + { + mcc: '3573', + description: 'SANDMAN HOTELS', + }, + { + mcc: '3574', + description: 'VENTURE INNS', + }, + { + mcc: '3575', + description: 'VAGABOND HOTELS', + }, + { + mcc: '3576', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3577', + description: 'MANDARIN ORIENTAL HOTEL', + }, + { + mcc: '3578', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3579', + description: 'HOTEL MERCURE', + }, + { + mcc: '3580', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3581', + description: 'DELTA HOTEL', + }, + { + mcc: '3582', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3583', + description: 'SAS HOTELS', + }, + { + mcc: '3584', + description: 'PRINCESS HOTELS INTERNATIONAL', + }, + { + mcc: '3585', + description: 'HUNGAR HOTELS', + }, + { + mcc: '3586', + description: 'SOKOS HOTELS', + }, + { + mcc: '3587', + description: 'DORAL HOTELS', + }, + { + mcc: '3588', + description: 'HELMSLEY HOTELS', + }, + { + mcc: '3589', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3590', + description: 'FAIRMONT HOTELS', + }, + { + mcc: '3591', + description: 'SONESTA HOTELS', + }, + { + mcc: '3592', + description: 'OMNI HOTELS', + }, + { + mcc: '3593', + description: 'CUNARD HOTELS', + }, + { + mcc: '3594', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3595', + description: 'HOSPITALITY INTERNATIONAL', + }, + { + mcc: '3596', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3597', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3598', + description: 'REGENT INTERNATIONAL HOTELS', + }, + { + mcc: '3599', + description: 'PANNONIA HOTELS', + }, + { + mcc: '3600', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3601', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3602', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3603', + description: 'NOAH’S HOTELS', + }, + { + mcc: '3604', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3605', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3606', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3607', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3608', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3609', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3610', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3611', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3612', + description: 'MOVENPICK HOTELS', + }, + { + mcc: '3613', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3614', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3615', + description: 'TRAVELODGE', + }, + { + mcc: '3616', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3617', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3618', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3619', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3620', + description: 'TELFORD INTERNATIONAL', + }, + { + mcc: '3621', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3622', + description: 'MERLIN HOTELS', + }, + { + mcc: '3623', + description: 'DORINT HOTELS', + }, + { + mcc: '3624', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3625', + description: 'HOTLE UNIVERSALE', + }, + { + mcc: '3626', + description: 'PRINCE HOTELS', + }, + { + mcc: '3627', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3628', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3629', + description: 'DAN HOTELS', + }, + { + mcc: '3630', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3631', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3632', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3633', + description: 'RANK HOTELS', + }, + { + mcc: '3634', + description: 'SWISSOTEL', + }, + { + mcc: '3635', + description: 'RESO HOTELS', + }, + { + mcc: '3636', + description: 'SAROVA HOTELS', + }, + { + mcc: '3637', + description: 'RAMADA INNS, RAMADA LIMITED', + }, + { + mcc: '3638', + description: 'HO JO INN, HOWARD JOHNSON', + }, + { + mcc: '3639', + description: 'MOUNT CHARLOTTE THISTLE', + }, + { + mcc: '3640', + description: 'HYATT HOTEL', + }, + { + mcc: '3641', + description: 'SOFITEL HOTELS', + }, + { + mcc: '3642', + description: 'NOVOTEL HOTELS', + }, + { + mcc: '3643', + description: 'STEIGENBERGER HOTELS', + }, + { + mcc: '3644', + description: 'ECONO LODGES', + }, + { + mcc: '3645', + description: 'QUEENS MOAT HOUSES', + }, + { + mcc: '3646', + description: 'SWALLOW HOTELS', + }, + { + mcc: '3647', + description: 'HUSA HOTELS', + }, + { + mcc: '3648', + description: 'DE VERE HOTELS', + }, + { + mcc: '3649', + description: 'RADISSON HOTELS', + }, + { + mcc: '3650', + description: 'RED ROOK INNS', + }, + { + mcc: '3651', + description: 'IMPERIAL LONDON HOTEL', + }, + { + mcc: '3652', + description: 'EMBASSY HOTELS', + }, + { + mcc: '3653', + description: 'PENTA HOTELS', + }, + { + mcc: '3654', + description: 'LOEWS HOTELS', + }, + { + mcc: '3655', + description: 'SCANDIC HOTELS', + }, + { + mcc: '3656', + description: 'SARA HOTELS', + }, + { + mcc: '3657', + description: 'OBEROI HOTELS', + }, + { + mcc: '3658', + description: 'OTANI HOTELS', + }, + { + mcc: '3659', + description: 'TAJ HOTELS INTERNATIONAL', + }, + { + mcc: '3660', + description: 'KNIGHTS INNS', + }, + { + mcc: '3661', + description: 'METROPOLE HOTELS', + }, + { + mcc: '3662', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3663', + description: 'HOTELES EL PRESIDENTS', + }, + { + mcc: '3664', + description: 'FLAG INN', + }, + { + mcc: '3665', + description: 'HAMPTON INNS', + }, + { + mcc: '3666', + description: 'STAKIS HOTELS', + }, + { + mcc: '3667', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3668', + description: 'MARITIM HOTELS', + }, + { + mcc: '3669', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3670', + description: 'ARCARD HOTELS', + }, + { + mcc: '3671', + description: 'ARCTIA HOTELS', + }, + { + mcc: '3672', + description: 'CAMPANIEL HOTELS', + }, + { + mcc: '3673', + description: 'IBUSZ HOTELS', + }, + { + mcc: '3674', + description: 'RANTASIPI HOTELS', + }, + { + mcc: '3675', + description: 'INTERHOTEL CEDOK', + }, + { + mcc: '3676', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3677', + description: 'CLIMAT DE FRANCE HOTELS', + }, + { + mcc: '3678', + description: 'CUMULUS HOTELS', + }, + { + mcc: '3679', + description: 'DANUBIUS HOTEL', + }, + { + mcc: '3680', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3681', + description: 'ADAMS MARK HOTELS', + }, + { + mcc: '3682', + description: 'ALLSTAR INNS', + }, + { + mcc: '3683', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3684', + description: 'BUDGET HOST INNS', + }, + { + mcc: '3685', + description: 'BUDGETEL HOTELS', + }, + { + mcc: '3686', + description: 'SUISSE CHALETS', + }, + { + mcc: '3687', + description: 'CLARION HOTELS', + }, + { + mcc: '3688', + description: 'COMPRI HOTELS', + }, + { + mcc: '3689', + description: 'CONSORT HOTELS', + }, + { + mcc: '3690', + description: 'COURTYARD BY MARRIOTT', + }, + { + mcc: '3691', + description: 'DILLION INNS', + }, + { + mcc: '3692', + description: 'DOUBLETREE HOTELS', + }, + { + mcc: '3693', + description: 'DRURY INNS', + }, + { + mcc: '3694', + description: 'ECONOMY INNS OF AMERICA', + }, + { + mcc: '3695', + description: 'EMBASSY SUITES', + }, + { + mcc: '3696', + description: 'EXEL INNS', + }, + { + mcc: '3697', + description: 'FARFIELD HOTELS', + }, + { + mcc: '3698', + description: 'HARLEY HOTELS', + }, + { + mcc: '3699', + description: 'MIDWAY MOTOR LODGE', + }, + { + mcc: '3700', + description: 'MOTEL 6', + }, + { + mcc: '3701', + description: 'GUEST QUARTERS (Formally PICKETT SUITE HOTELS)', + }, + { + mcc: '3702', + description: 'THE REGISTRY HOTELS', + }, + { + mcc: '3703', + description: 'RESIDENCE INNS', + }, + { + mcc: '3704', + description: 'ROYCE HOTELS', + }, + { + mcc: '3705', + description: 'SANDMAN INNS', + }, + { + mcc: '3706', + description: 'SHILO INNS', + }, + { + mcc: '3707', + description: 'SHONEY’S INNS', + }, + { + mcc: '3708', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3709', + description: 'SUPER8 MOTELS', + }, + { + mcc: '3710', + description: 'THE RITZ CARLTON HOTELS', + }, + { + mcc: '3711', + description: 'FLAG INNS (AUSRALIA)', + }, + { + mcc: '3712', + description: 'GOLDEN CHAIN HOTEL', + }, + { + mcc: '3713', + description: 'QUALITY PACIFIC HOTEL', + }, + { + mcc: '3714', + description: 'FOUR SEASONS HOTEL (AUSTRALIA)', + }, + { + mcc: '3715', + description: 'FARIFIELD INN', + }, + { + mcc: '3716', + description: 'CARLTON HOTELS', + }, + { + mcc: '3717', + description: 'CITY LODGE HOTELS', + }, + { + mcc: '3718', + description: 'KAROS HOTELS', + }, + { + mcc: '3719', + description: 'PROTEA HOTELS', + }, + { + mcc: '3720', + description: 'SOUTHERN SUN HOTELS', + }, + { + mcc: '3721', + description: 'HILTON CONRAD', + }, + { + mcc: '3722', + description: 'WYNDHAM HOTEL AND RESORTS', + }, + { + mcc: '3723', + description: 'RICA HOTELS', + }, + { + mcc: '3724', + description: 'INER NOR HOTELS', + }, + { + mcc: '3725', + description: 'SEAINES PLANATION', + }, + { + mcc: '3726', + description: 'RIO SUITES', + }, + { + mcc: '3727', + description: 'BROADMOOR HOTEL', + }, + { + mcc: '3728', + description: 'BALLY’S HOTEL AND CASINO', + }, + { + mcc: '3729', + description: 'JOHN ASCUAGA’S NUGGET', + }, + { + mcc: '3730', + description: 'MGM GRAND HOTEL', + }, + { + mcc: '3731', + description: 'HARRAH’S HOTELS AND CASINOS', + }, + { + mcc: '3732', + description: 'OPRYLAND HOTEL', + }, + { + mcc: '3733', + description: 'BOCA RATON RESORT', + }, + { + mcc: '3734', + description: 'HARVEY/BRISTOL HOTELS', + }, + { + mcc: '3735', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3736', + description: 'COLORADO BELLE/EDGEWATER RESORT', + }, + { + mcc: '3737', + description: 'RIVIERA HOTEL AND CASINO', + }, + { + mcc: '3738', + description: 'TROPICANA RESORT AND CASINO', + }, + { + mcc: '3739', + description: 'WOODSIDE HOTELS AND RESORTS', + }, + { + mcc: '3740', + description: 'TOWNPLACE SUITES', + }, + { + mcc: '3741', + description: 'MILLENIUM BROADWAY HOTEL', + }, + { + mcc: '3742', + description: 'CLUB MED', + }, + { + mcc: '3743', + description: 'BILTMORE HOTEL AND SUITES', + }, + { + mcc: '3744', + description: 'CAREFREE RESORTS', + }, + { + mcc: '3745', + description: 'ST. REGIS HOTEL', + }, + { + mcc: '3746', + description: 'THE ELIOT HOTEL', + }, + { + mcc: '3747', + description: 'CLUBCORP/CLUB RESORTS', + }, + { + mcc: '3748', + description: 'WELESLEY INNS', + }, + { + mcc: '3749', + description: 'THE BEVERLY HILLS HOTEL', + }, + { + mcc: '3750', + description: 'CROWNE PLAZA HOTELS', + }, + { + mcc: '3751', + description: 'HOMEWOOD SUITES', + }, + { + mcc: '3752', + description: 'PEABODY HOTELS', + }, + { + mcc: '3753', + description: 'GREENBRIAH RESORTS', + }, + { + mcc: '3754', + description: 'AMELIA ISLAND PLANATION', + }, + { + mcc: '3755', + description: 'THE HOMESTEAD', + }, + { + mcc: '3756', + description: 'SOUTH SEAS RESORTS', + }, + { + mcc: '3757', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3758', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3759', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3760', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3761', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3762', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3763', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3764', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3765', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3766', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3767', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3768', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3769', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3770', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3771', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3772', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3773', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3774', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3775', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3776', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3777', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3778', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3779', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3780', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3781', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3782', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3783', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3784', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3785', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3786', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3787', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3788', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3789', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3790', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3816', + description: 'Home2Suites', + }, + { + mcc: '3835', + description: '* MASTERS ECONOMY INNS', + }, + { + mcc: '4011', + description: 'Railroads', + }, + { + mcc: '4111', + description: + 'Local/Suburban Commuter Passenger Transportation – Railroads, Feries, Local Water Transportation.', + }, + { + mcc: '4112', + description: 'Passenger Railways', + }, + { + mcc: '4119', + description: 'Ambulance Services', + }, + { + mcc: '4121', + description: 'Taxicabs and Limousines', + }, + { + mcc: '4131', + description: 'Bus Lines, Including Charters, Tour Buses', + }, + { + mcc: '4214', + description: + 'Motor Freight Carriers, Moving and Storage Companies, Trucking – Local/Long Distance, Delivery Services – Local', + }, + { + mcc: '4215', + description: 'Courier Services – Air or Ground, Freight forwarders', + }, + { + mcc: '4225', + description: 'Public warehousing, Storage', + }, + { + mcc: '4411', + description: 'Cruise and Steamship Lines', + }, + { + mcc: '4457', + description: 'Boat Rentals and Leases', + }, + { + mcc: '4468', + description: 'Marinas, Marine Service, and Supplies', + }, + { + mcc: '4511', + description: 'Airlines, Air Carriers ( not listed elsewhere)', + }, + { + mcc: '4582', + description: 'Airports, Airport Terminals, Flying Fields', + }, + { + mcc: '4722', + description: 'Travel Agencies and Tour Operations', + }, + { + mcc: '4723', + description: 'Package Tour Operators (For use in Germany only)', + }, + { + mcc: '4784', + description: 'Toll and Bridge Fees', + }, + { + mcc: '4789', + description: 'Transportation Services, Not elsewhere classified)', + }, + { + mcc: '4812', + description: 'Telecommunications Equipment including telephone sales', + }, + { + mcc: '4814', + description: 'Fax services, Telecommunication Services', + }, + { + mcc: '4815', + description: 'VisaPhone', + }, + { + mcc: '4816', + description: 'Computer Network Services', + }, + { + mcc: '4821', + description: 'Telegraph services', + }, + { + mcc: '4829', + description: 'Money Orders – Wire Transfer', + }, + { + mcc: '4899', + description: 'Cable and other pay television (previously Cable Services)', + }, + { + mcc: '4900', + description: 'Electric, Gas, Sanitary and Water Utilities', + }, + { + mcc: '5013', + description: 'Motor vehicle supplies and new parts', + }, + { + mcc: '5021', + description: 'Office and Commercial Furniture', + }, + { + mcc: '5039', + description: 'Construction Materials, Not Elsewhere Classified', + }, + { + mcc: '5044', + description: 'Office, Photographic, Photocopy, and Microfilm Equipment', + }, + { + mcc: '5045', + description: 'Computers, Computer Peripheral Equipment, Software', + }, + { + mcc: '5046', + description: 'Commercial Equipment, Not Elsewhere Classified', + }, + { + mcc: '5047', + description: 'Medical, Dental Ophthalmic, Hospital Equipment and Supplies', + }, + { + mcc: '5051', + description: 'Metal Service Centers and Offices', + }, + { + mcc: '5065', + description: 'Electrical Parts and Equipment', + }, + { + mcc: '5072', + description: 'Hardware Equipment and Supplies', + }, + { + mcc: '5074', + description: 'Plumbing and Heating Equipment and Supplies', + }, + { + mcc: '5085', + description: 'Industrial Supplies, Not Elsewhere Classified', + }, + { + mcc: '5094', + description: 'Precious Stones and Metals, Watches and Jewelry', + }, + { + mcc: '5099', + description: 'Durable Goods, Not Elsewhere Classified', + }, + { + mcc: '5111', + description: 'Stationery, Office Supplies, Printing, and Writing Paper', + }, + { + mcc: '5122', + description: 'Drugs, Drug Proprietors, and Druggist’s Sundries', + }, + { + mcc: '5131', + description: 'Piece Goods, Notions, and Other Dry Goods', + }, + { + mcc: '5137', + description: 'Men’s Women’s and Children’s Uniforms and Commercial Clothing', + }, + { + mcc: '5139', + description: 'Commercial Footwear', + }, + { + mcc: '5169', + description: 'Chemicals and Allied Products, Not Elsewhere Classified', + }, + { + mcc: '5172', + description: 'Petroleum and Petroleum Products', + }, + { + mcc: '5192', + description: 'Books, Periodicals, and Newspapers', + }, + { + mcc: '5193', + description: 'Florists’ Supplies, Nursery Stock and Flowers', + }, + { + mcc: '5198', + description: 'Paints, Varnishes, and Supplies', + }, + { + mcc: '5199', + description: 'Non-durable Goods, Not Elsewhere Classified', + }, + { + mcc: '5200', + description: 'Home Supply Warehouse Stores', + }, + { + mcc: '5211', + description: 'Lumber and Building Materials Stores', + }, + { + mcc: '5231', + description: 'Glass, Paint, and Wallpaper Stores', + }, + { + mcc: '5251', + description: 'Hardware Stores', + }, + { + mcc: '5261', + description: 'Nurseries – Lawn and Garden Supply Store', + }, + { + mcc: '5271', + description: 'Mobile Home Dealers', + }, + { + mcc: '5300', + description: 'Wholesale Clubs', + }, + { + mcc: '5309', + description: 'Duty Free Store', + }, + { + mcc: '5310', + description: 'Discount Stores', + }, + { + mcc: '5311', + description: 'Department Stores', + }, + { + mcc: '5331', + description: 'Variety Stores', + }, + { + mcc: '5399', + description: 'Misc. General Merchandise', + }, + { + mcc: '5411', + description: 'Grocery Stores, Supermarkets', + }, + { + mcc: '5422', + description: 'Meat Provisioners – Freezer and Locker', + }, + { + mcc: '5441', + description: 'Candy, Nut, and Confectionery Stores', + }, + { + mcc: '5451', + description: 'Dairy Products Stores', + }, + { + mcc: '5462', + description: 'Bakeries', + }, + { + mcc: '5499', + description: 'Misc. Food Stores – Convenience Stores and Specialty Markets', + }, + { + mcc: '5511', + description: 'Car and Truck Dealers (New and Used) Sales, Service, Repairs, Parts, and Leasing', + }, + { + mcc: '5521', + description: 'Automobile and Truck Dealers (Used Only)', + }, + { + mcc: '5531', + description: 'Automobile Supply Stores', + }, + { + mcc: '5532', + description: 'Automotive Tire Stores', + }, + { + mcc: '5533', + description: 'Automotive Parts, Accessories Stores', + }, + { + mcc: '5541', + description: 'Service Stations ( with or without ancillary services)', + }, + { + mcc: '5542', + description: 'Automated Fuel Dispensers', + }, + { + mcc: '5551', + description: 'Boat Dealers', + }, + { + mcc: '5561', + description: 'Recreational and Utility Trailers, Camp Dealers', + }, + { + mcc: '5571', + description: 'Motorcycle Dealers', + }, + { + mcc: '5592', + description: 'Motor Home Dealers', + }, + { + mcc: '5598', + description: 'Snowmobile Dealers', + }, + { + mcc: '5599', + description: 'Miscellaneous Auto Dealers ', + }, + { + mcc: '5611', + description: 'Men’s and Boy’s Clothing and Accessories Stores', + }, + { + mcc: '5621', + description: 'Women’s Ready-to-Wear Stores', + }, + { + mcc: '5631', + description: 'Women’s Accessory and Specialty Shops', + }, + { + mcc: '5641', + description: 'Children’s and Infant’s Wear Stores', + }, + { + mcc: '5651', + description: 'Family Clothing Stores', + }, + { + mcc: '5655', + description: 'Sports Apparel, Riding Apparel Stores', + }, + { + mcc: '5661', + description: 'Shoe Stores', + }, + { + mcc: '5681', + description: 'Furriers and Fur Shops', + }, + { + mcc: '5691', + description: 'Men’s and Women’s Clothing Stores', + }, + { + mcc: '5697', + description: 'Tailors, Seamstress, Mending, and Alterations', + }, + { + mcc: '5698', + description: 'Wig and Toupee Stores', + }, + { + mcc: '5699', + description: 'Miscellaneous Apparel and Accessory Shops', + }, + { + mcc: '5712', + description: 'Furniture, Home Furnishings, and Equipment Stores, ExceptAppliances', + }, + { + mcc: '5713', + description: 'Floor Covering Stores', + }, + { + mcc: '5714', + description: 'Drapery, Window Covering and Upholstery Stores', + }, + { + mcc: '5718', + description: 'Fireplace, Fireplace Screens, and Accessories Stores', + }, + { + mcc: '5719', + description: 'Miscellaneous Home Furnishing Specialty Stores', + }, + { + mcc: '5722', + description: 'Household Appliance Stores', + }, + { + mcc: '5732', + description: 'Electronic Sales', + }, + { + mcc: '5733', + description: 'Music Stores, Musical Instruments, Piano Sheet Music', + }, + { + mcc: '5734', + description: 'Computer Software Stores', + }, + { + mcc: '5735', + description: 'Record Shops', + }, + { + mcc: '5811', + description: 'Caterers', + }, + { + mcc: '5812', + description: 'Eating places and Restaurants', + }, + { + mcc: '5813', + description: + 'Drinking Places (Alcoholic Beverages), Bars, Taverns, Cocktail lounges, Nightclubs and Discotheques', + }, + { + mcc: '5814', + description: 'Fast Food Restaurants', + }, + { + mcc: '5815', + description: 'Digital Goods: Media, Books, Movies, Music', + }, + { + mcc: '5816', + description: 'Digital Goods: Games', + }, + { + mcc: '5817', + description: 'Digital Goods: Applications (Excludes Games)', + }, + { + mcc: '5818', + description: 'Digital Goods: Large Digital Goods Merchant', + }, + { + mcc: '5832', + description: 'Antique Shops – Sales, Repairs, and Restoration Services', + }, + { + mcc: '5912', + description: 'Drug Stores and Pharmacies', + }, + { + mcc: '5921', + description: 'Package Stores – Beer, Wine, and Liquor', + }, + { + mcc: '5931', + description: 'Used Merchandise and Secondhand Stores', + }, + { + mcc: '5932', + description: 'Antique Shops', + }, + { + mcc: '5933', + description: 'Pawn Shops and Salvage Yards', + }, + { + mcc: '5935', + description: 'Wrecking and Salvage Yards', + }, + { + mcc: '5937', + description: 'Antique Reproductions', + }, + { + mcc: '5940', + description: 'Bicycle Shops – Sales and Service', + }, + { + mcc: '5941', + description: 'Sporting Goods Stores', + }, + { + mcc: '5942', + description: 'Book Stores', + }, + { + mcc: '5943', + description: 'Stationery Stores, Office and School Supply Stores', + }, + { + mcc: '5944', + description: 'Watch, Clock, Jewelry, and Silverware Stores', + }, + { + mcc: '5945', + description: 'Hobby, Toy, and Game Shops', + }, + { + mcc: '5946', + description: 'Camera and Photographic Supply Stores', + }, + { + mcc: '5947', + description: 'Card Shops, Gift, Novelty, and Souvenir Shops', + }, + { + mcc: '5948', + description: 'Leather Goods Stores', + }, + { + mcc: '5949', + description: 'Sewing, Needle, Fabric, and Price Goods Stores', + }, + { + mcc: '5950', + description: 'Glassware/Crystal Stores', + }, + { + mcc: '5960', + description: 'Direct Marketing- Insurance Service', + }, + { + mcc: '5961', + description: + 'Mail Order Houses Including Catalog Order Stores, Book/Record Clubs (No longer permitted for U.S. original presentments)', + }, + { + mcc: '5962', + description: 'Direct Marketing – Travel Related Arrangements Services', + }, + { + mcc: '5963', + description: 'Door-to-Door Sales', + }, + { + mcc: '5964', + description: 'Direct Marketing – Catalog Merchant', + }, + { + mcc: '5965', + description: 'Direct Marketing – Catalog and Catalog and Retail Merchant', + }, + { + mcc: '5966', + description: 'Direct Marketing- Outbound Telemarketing Merchant', + }, + { + mcc: '5967', + description: 'Direct Marketing – Inbound Teleservices Merchant', + }, + { + mcc: '5968', + description: 'Direct Marketing – Continuity/Subscription Merchant', + }, + { + mcc: '5969', + description: 'Direct Marketing – Not Elsewhere Classified', + }, + { + mcc: '5970', + description: 'Artist’s Supply and Craft Shops', + }, + { + mcc: '5971', + description: 'Art Dealers and Galleries', + }, + { + mcc: '5972', + description: 'Stamp and Coin Stores – Philatelic and Numismatic Supplies', + }, + { + mcc: '5973', + description: 'Religious Goods Stores', + }, + { + mcc: '5975', + description: 'Hearing Aids – Sales, Service, and Supply Stores', + }, + { + mcc: '5976', + description: 'Orthopedic Goods Prosthetic Devices', + }, + { + mcc: '5977', + description: 'Cosmetic Stores', + }, + { + mcc: '5978', + description: 'Typewriter Stores – Sales, Rental, Service', + }, + { + mcc: '5983', + description: 'Fuel – Fuel Oil, Wood, Coal, Liquefied Petroleum', + }, + { + mcc: '5992', + description: 'Florists', + }, + { + mcc: '5993', + description: 'Cigar Stores and Stands', + }, + { + mcc: '5994', + description: 'News Dealers and Newsstands', + }, + { + mcc: '5995', + description: 'Pet Shops, Pet Foods, and Supplies Stores', + }, + { + mcc: '5996', + description: 'Swimming Pools – Sales, Service, and Supplies', + }, + { + mcc: '5997', + description: 'Electric Razor Stores – Sales and Service', + }, + { + mcc: '5998', + description: 'Tent and Awning Shops', + }, + { + mcc: '5999', + description: 'Miscellaneous and Specialty Retail Stores', + }, + { + mcc: '6010', + description: 'Financial Institutions – Manual Cash Disbursements', + }, + { + mcc: '6011', + description: 'Financial Institutions – Manual Cash Disbursements', + }, + { + mcc: '6012', + description: 'Financial Institutions – Merchandise and Services', + }, + { + mcc: '6051', + description: + 'Non-Financial Institutions – Foreign Currency, Money Orders (not wire transfer) and Travelers Cheques', + }, + { + mcc: '6211', + description: 'Security Brokers/Dealers', + }, + { + mcc: '6300', + description: 'Insurance Sales, Underwriting, and Premiums', + }, + { + mcc: '6381', + description: 'Insurance Premiums, (no longer valid for first presentment work)', + }, + { + mcc: '6399', + description: 'Insurance, Not Elsewhere Classified ( no longer valid forfirst presentment work)', + }, + { + mcc: '6513', + description: 'Real Estate Agents and Managers - Rentals', + }, + { + mcc: '7011', + description: + 'Lodging – Hotels, Motels, Resorts, Central Reservation Services (not elsewhere classified)', + }, + { + mcc: '7012', + description: 'Timeshares', + }, + { + mcc: '7032', + description: 'Sporting and Recreational Camps', + }, + { + mcc: '7033', + description: 'Trailer Parks and Camp Grounds', + }, + { + mcc: '7210', + description: 'Laundry, Cleaning, and Garment Services', + }, + { + mcc: '7211', + description: 'Laundry – Family and Commercial', + }, + { + mcc: '7216', + description: 'Dry Cleaners', + }, + { + mcc: '7217', + description: 'Carpet and Upholstery Cleaning', + }, + { + mcc: '7221', + description: 'Photographic Studios', + }, + { + mcc: '7230', + description: 'Barber and Beauty Shops', + }, + { + mcc: '7251', + description: 'Shop Repair Shops and Shoe Shine Parlors, and Hat Cleaning Shops', + }, + { + mcc: '7261', + description: 'Funeral Service and Crematories', + }, + { + mcc: '7273', + description: 'Dating and Escort Services', + }, + { + mcc: '7276', + description: 'Tax Preparation Service', + }, + { + mcc: '7277', + description: 'Counseling Service – Debt, Marriage, Personal', + }, + { + mcc: '7278', + description: 'Buying/Shopping Services, Clubs', + }, + { + mcc: '7296', + description: 'Clothing Rental – Costumes, Formal Wear, Uniforms', + }, + { + mcc: '7297', + description: 'Massage Parlors', + }, + { + mcc: '7298', + description: 'Health and Beauty Shops', + }, + { + mcc: '7299', + description: 'Miscellaneous Personal Services ( not elsewhere classifies)', + }, + { + mcc: '7311', + description: 'Advertising Services', + }, + { + mcc: '7321', + description: 'Consumer Credit Reporting Agencies', + }, + { + mcc: '7332', + description: 'Blueprinting and Photocopying Services', + }, + { + mcc: '7333', + description: 'Commercial Photography, Art and Graphics', + }, + { + mcc: '7338', + description: 'Quick Copy, Reproduction and Blueprinting Services', + }, + { + mcc: '7339', + description: 'Stenographic and Secretarial Support Services', + }, + { + mcc: '7342', + description: 'Exterminating and Disinfecting Services', + }, + { + mcc: '7349', + description: 'Cleaning and Maintenance, Janitorial Services', + }, + { + mcc: '7361', + description: 'Employment Agencies, Temporary Help Services', + }, + { + mcc: '7372', + description: 'Computer Programming, Integrated Systems Design and Data Processing Services', + }, + { + mcc: '7375', + description: 'Information Retrieval Services', + }, + { + mcc: '7379', + description: 'Computer Maintenance and Repair Services, Not Elsewhere Classified', + }, + { + mcc: '7392', + description: 'Management, Consulting, and Public Relations Services', + }, + { + mcc: '7393', + description: 'Protective and Security Services – Including Armored Carsand Guard Dogs', + }, + { + mcc: '7394', + description: + 'Equipment Rental and Leasing Services, Tool Rental, Furniture Rental, and Appliance Rental', + }, + { + mcc: '7395', + description: 'Photofinishing Laboratories, Photo Developing', + }, + { + mcc: '7399', + description: 'Business Services, Not Elsewhere Classified', + }, + { + mcc: '7511', + description: 'Truck Stop', + }, + { + mcc: '7512', + description: 'Car Rental Companies ( Not Listed Below)', + }, + { + mcc: '7513', + description: 'Truck and Utility Trailer Rentals', + }, + { + mcc: '7519', + description: 'Motor Home and Recreational Vehicle Rentals', + }, + { + mcc: '7523', + description: 'Automobile Parking Lots and Garages', + }, + { + mcc: '7531', + description: 'Automotive Body Repair Shops', + }, + { + mcc: '7534', + description: 'Tire Re-treading and Repair Shops', + }, + { + mcc: '7535', + description: 'Paint Shops – Automotive', + }, + { + mcc: '7538', + description: 'Automotive Service Shops', + }, + { + mcc: '7542', + description: 'Car Washes', + }, + { + mcc: '7549', + description: 'Towing Services', + }, + { + mcc: '7622', + description: 'Radio Repair Shops', + }, + { + mcc: '7623', + description: 'Air Conditioning and Refrigeration Repair Shops', + }, + { + mcc: '7629', + description: 'Electrical And Small Appliance Repair Shops', + }, + { + mcc: '7631', + description: 'Watch, Clock, and Jewelry Repair', + }, + { + mcc: '7641', + description: 'Furniture, Furniture Repair, and Furniture Refinishing', + }, + { + mcc: '7692', + description: 'Welding Repair', + }, + { + mcc: '7699', + description: 'Repair Shops and Related Services –Miscellaneous', + }, + { + mcc: '7800', + description: 'Government-Owned Lotteries', + }, + { + mcc: '7801', + description: 'Government-Licensed On-Line Casinos (On-Line Gambling)', + }, + { + mcc: '7802', + description: 'Government-Licensed Horse/Dog Racing', + }, + { + mcc: '7829', + description: 'Motion Pictures and Video Tape Production and Distribution', + }, + { + mcc: '7832', + description: 'Motion Picture Theaters', + }, + { + mcc: '7841', + description: 'Video Tape Rental Stores', + }, + { + mcc: '7911', + description: 'Dance Halls, Studios and Schools', + }, + { + mcc: '7922', + description: 'Theatrical Producers (Except Motion Pictures), Ticket Agencies', + }, + { + mcc: '7929', + description: 'Bands, Orchestras, and Miscellaneous Entertainers (Not Elsewhere Classified)', + }, + { + mcc: '7932', + description: 'Billiard and Pool Establishments', + }, + { + mcc: '7933', + description: 'Bowling Alleys', + }, + { + mcc: '7941', + description: + 'Commercial Sports, Athletic Fields, Professional Sport Clubs, and Sport Promoters', + }, + { + mcc: '7991', + description: 'Tourist Attractions and Exhibits', + }, + { + mcc: '7992', + description: 'Golf Courses – Public', + }, + { + mcc: '7993', + description: 'Video Amusement Game Supplies', + }, + { + mcc: '7994', + description: 'Video Game Arcades/Establishments', + }, + { + mcc: '7995', + description: + 'Betting (including Lottery Tickets, Casino Gaming Chips, Off-track Betting and Wagers at Race Tracks)', + }, + { + mcc: '7996', + description: 'Amusement Parks, Carnivals, Circuses, Fortune Tellers', + }, + { + mcc: '7997', + description: + 'Membership Clubs (Sports, Recreation, Athletic), Country Clubs, and Private Golf Courses', + }, + { + mcc: '7998', + description: 'Aquariums, Sea-aquariums, Dolphinariums', + }, + { + mcc: '7999', + description: 'Recreation Services (Not Elsewhere Classified)', + }, + { + mcc: '8011', + description: 'Doctors and Physicians (Not Elsewhere Classified)', + }, + { + mcc: '8021', + description: 'Dentists and Orthodontists', + }, + { + mcc: '8031', + description: 'Osteopaths', + }, + { + mcc: '8041', + description: 'Chiropractors', + }, + { + mcc: '8042', + description: 'Optometrists and Ophthalmologists', + }, + { + mcc: '8043', + description: 'Opticians, Opticians Goods and Eyeglasses', + }, + { + mcc: '8044', + description: 'Opticians, Optical Goods, and Eyeglasses (no longer validfor first presentments)', + }, + { + mcc: '8049', + description: 'Podiatrists and Chiropodists', + }, + { + mcc: '8050', + description: 'Nursing and Personal Care Facilities', + }, + { + mcc: '8062', + description: 'Hospitals', + }, + { + mcc: '8071', + description: 'Medical and Dental Laboratories', + }, + { + mcc: '8099', + description: 'Medical Services and Health Practitioners (Not Elsewhere Classified)', + }, + { + mcc: '8111', + description: 'Legal Services and Attorneys', + }, + { + mcc: '8211', + description: 'Elementary and Secondary Schools', + }, + { + mcc: '8220', + description: 'Colleges, Junior Colleges, Universities, and ProfessionalSchools', + }, + { + mcc: '8241', + description: 'Correspondence Schools', + }, + { + mcc: '8244', + description: 'Business and Secretarial Schools', + }, + { + mcc: '8249', + description: 'Vocational Schools and Trade Schools', + }, + { + mcc: '8299', + description: 'Schools and Educational Services ( Not Elsewhere Classified)', + }, + { + mcc: '8351', + description: 'Child Care Services', + }, + { + mcc: '8398', + description: 'Charitable and Social Service Organizations', + }, + { + mcc: '8641', + description: 'Civic, Fraternal, and Social Associations', + }, + { + mcc: '8651', + description: 'Political Organizations', + }, + { + mcc: '8661', + description: 'Religious Organizations', + }, + { + mcc: '8675', + description: 'Automobile Associations', + }, + { + mcc: '8699', + description: 'Membership Organizations ( Not Elsewhere Classified)', + }, + { + mcc: '8734', + description: 'Testing Laboratories ( non-medical)', + }, + { + mcc: '8911', + description: 'Architectural – Engineering and Surveying Services', + }, + { + mcc: '8931', + description: 'Accounting, Auditing, and Bookkeeping Services', + }, + { + mcc: '8999', + description: 'Professional Services ( Not Elsewhere Defined)', + }, + { + mcc: '9211', + description: 'Court Costs, including Alimony and Child Support', + }, + { + mcc: '9222', + description: 'Fines', + }, + { + mcc: '9223', + description: 'Bail and Bond Payments', + }, + { + mcc: '9311', + description: 'Tax Payments', + }, + { + mcc: '9399', + description: 'Government Services ( Not Elsewhere Classified)', + }, + { + mcc: '9402', + description: 'Postal Services – Government Only', + }, + { + mcc: '9405', + description: 'Intra – Government Transactions', + }, + { + mcc: '9700', + description: 'Automated Referral Service ( For Visa Only)', + }, + { + mcc: '9701', + description: 'Visa Credential Service ( For Visa Only)', + }, + { + mcc: '9702', + description: 'GCAS Emergency Services ( For Visa Only)', + }, + { + mcc: '9950', + description: 'Intra – Company Purchases ( For Visa Only)', + }, +].map(item => ({ + value: item.mcc, + label: item.description, +})); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx new file mode 100644 index 0000000000..c61f16b916 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx @@ -0,0 +1,71 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { + Chip, + createTestId, + MultiSelect, + MultiSelectSelectedItemRenderer, + MultiSelectValue, +} from '@ballerine/ui'; +import { X } from 'lucide-react'; +import { FunctionComponent, useCallback, useMemo } from 'react'; + +export interface IMultiselectFieldOption { + label: string; + value: string; +} + +export interface IMultiselectFieldOptions { + options: IMultiselectFieldOption[]; + placeholder?: string; +} + +export const MultiselectField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options: _options, stack }) => { + const { value, disabled, onChange, onBlur } = fieldProps; + const { options, placeholder } = _options || {}; + + const multiselectOptions = useMemo( + () => + options.map(option => ({ + title: option.label, + value: option.value, + })), + [options], + ); + + const handleChange = useCallback( + (selected: MultiSelectValue[]) => { + onChange(selected); + }, + [onChange], + ); + + const renderSelected: MultiSelectSelectedItemRenderer = useCallback((params, option) => { + return ( + + + } + /> + + ); + }, []); + + return ( + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/index.ts new file mode 100644 index 0000000000..2009deaf54 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/index.ts @@ -0,0 +1 @@ +export * from './MultiselectField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/NationalityField/NationalityField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/NationalityField/NationalityField.tsx new file mode 100644 index 0000000000..d414d32e31 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/NationalityField/NationalityField.tsx @@ -0,0 +1,46 @@ +import { getNationalities } from '@/helpers/countries-data'; +import { useLanguageParam } from '@/hooks/useLanguageParam/useLanguageParam'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import { FunctionComponent, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface INationalityFieldOptions { + placeholder?: string; +} + +export const NationalityField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options, stack }) => { + const { value, disabled, onChange, onBlur } = fieldProps; + const { placeholder = '' } = options || {}; + + const { language } = useLanguageParam(); + const { t } = useTranslation(); + + const nationalities = useMemo(() => getNationalities(language, t), [language, t]); + + const dropdownOptions = useMemo(() => { + return nationalities.map(({ const: constValue, title }) => ({ + label: title, + value: constValue, + })); + }, [nationalities]); + + return ( + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/NationalityField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/NationalityField/index.ts new file mode 100644 index 0000000000..4600526100 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/NationalityField/index.ts @@ -0,0 +1 @@ +export * from './NationalityField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx new file mode 100644 index 0000000000..14b5a280c5 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx @@ -0,0 +1,42 @@ +import { FieldErrors } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, PhoneNumberInput } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export type TPhoneFieldValueType = string | undefined; + +export interface IPhoneFieldOptions { + defaultCountry?: string; + enableSearch?: boolean; +} + +const defaultOptions: IPhoneFieldOptions = { + defaultCountry: 'us', + enableSearch: true, +}; + +export const PhoneField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, options = defaultOptions, stack, definition }) => { + const { + defaultCountry = defaultOptions.defaultCountry, + enableSearch = defaultOptions.enableSearch, + } = options; + const { disabled, value, onBlur, onChange } = fieldProps; + + return ( + + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/index.ts new file mode 100644 index 0000000000..dab745108a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/index.ts @@ -0,0 +1 @@ +export * from './PhoneField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/RelationshipField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/RelationshipField.tsx new file mode 100644 index 0000000000..ff5a7b2543 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/RelationshipField.tsx @@ -0,0 +1,50 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { relationshipOptions } from '@/pages/CollectionFlowV2/components/ui/fields/RelationshipField/relationship-options'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import get from 'lodash/get'; +import { FunctionComponent, useMemo } from 'react'; + +export interface IRelationshipFieldOptions { + placeholder?: string; + companyNameDestination?: string; + companyName?: string; +} + +const COMPANY_NAME_PLACEHOLDER = '{company_name}'; + +export const RelationshipField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options: _options, stack }) => { + const { value, onChange, onBlur } = fieldProps; + const { + placeholder = '', + companyNameDestination = '', + companyName: _companyName = 'N/A', + } = _options || {}; + + const { payload } = useStateManagerContext(); + const dropdownOptions = useMemo(() => { + const companyName = get(payload, companyNameDestination, _companyName); + + return relationshipOptions.map(option => ({ + label: option.label.replace(COMPANY_NAME_PLACEHOLDER, companyName), + value: option.value.replace(COMPANY_NAME_PLACEHOLDER, companyName), + })); + }, [payload, companyNameDestination, _companyName]); + + return ( + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/index.ts new file mode 100644 index 0000000000..8040c8b47c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/index.ts @@ -0,0 +1 @@ +export * from './RelationshipField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/relationship-options.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/relationship-options.ts new file mode 100644 index 0000000000..2623f49949 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RelationshipField/relationship-options.ts @@ -0,0 +1,31 @@ +export const relationshipOptions = [ + { label: 'Parent of {company_name}', value: 'Parent of {company_name}' }, + { label: 'Subsidiary of {company_name}', value: 'Subsidiary of {company_name}' }, + { label: 'Affiliate with {company_name}', value: 'Affiliate with {company_name}' }, + { + label: 'Joint Venture Partner with {company_name}', + value: 'Joint Venture Partner with {company_name}', + }, + { + label: 'Majority Shareholder in {company_name}', + value: 'Majority Shareholder in {company_name}', + }, + { + label: 'Minority Shareholder in {company_name}', + value: 'Minority Shareholder in {company_name}', + }, + { label: 'Strategic Partner to {company_name}', value: 'Strategic Partner to {company_name}' }, + { + label: 'Institutional Investor in {company_name}', + value: 'Institutional Investor in {company_name}', + }, + { label: 'Angel Investor in {company_name}', value: 'Angel Investor in {company_name}' }, + { + label: 'Venture Capital Investor in {company_name}', + value: 'Venture Capital Investor in {company_name}', + }, + { + label: 'Private Equity Investor in {company_name}', + value: 'Private Equity Investor in {company_name}', + }, +]; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/StateField/StateField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/StateField/StateField.tsx new file mode 100644 index 0000000000..8780abe507 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/StateField/StateField.tsx @@ -0,0 +1,57 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { getCountryStates } from '@/helpers/countries-data'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import get from 'lodash/get'; +import { FunctionComponent, useMemo } from 'react'; + +export interface IStateFieldOptions { + placeholder?: string; + countryCodePath?: string; + countryCode?: string; +} + +export const StateField: FunctionComponent> = ({ + fieldProps, + definition, + options = {}, + stack, +}) => { + const { value, disabled, onChange, onBlur } = fieldProps; + const { + placeholder = 'Select State', + countryCodePath = '', + countryCode: _defaultCountryCode = 'US', + } = options; + + const { payload } = useStateManagerContext(); + const dropdownOptions = useMemo(() => { + const countryCode = get(payload, countryCodePath, _defaultCountryCode) as string; + + return ( + countryCode + ? getCountryStates(countryCode).map(state => ({ title: state.name, const: state.isoCode })) + : [] + ).map(option => ({ + label: option.title, + value: option.const, + })); + }, [payload, countryCodePath, _defaultCountryCode]); + + return ( + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/StateField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/StateField/index.ts new file mode 100644 index 0000000000..03bc19f8b7 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/StateField/index.ts @@ -0,0 +1 @@ +export * from './StateField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx new file mode 100644 index 0000000000..e453b3d31f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx @@ -0,0 +1,45 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { Tag, TagInput } from 'emblor'; +import { FunctionComponent, useMemo, useState } from 'react'; + +export interface ITagsFieldOptions { + placeholder?: string; +} + +export const TagsField: FunctionComponent> = ({ + fieldProps, + definition, + options, + stack, +}) => { + const { value, onBlur, onChange } = fieldProps; + const { placeholder } = options || {}; + + const [activeTagIndex, setActiveTagIndex] = useState(null); + + const tags = useMemo(() => { + if (!Array.isArray(value)) return []; + + return value.map((tag, index) => { + return { + id: String(index), + text: String(tag), + } satisfies Tag; + }); + }, [value]); + + return ( + + onChange((tags as Tag[]).map(tag => tag.text))} + tags={tags} + activeTagIndex={activeTagIndex} + setActiveTagIndex={setActiveTagIndex} + placeholder={placeholder} + addTagsOnBlur + /> + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/index.ts new file mode 100644 index 0000000000..ba8821d39e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/index.ts @@ -0,0 +1 @@ +export * from './TagsField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx new file mode 100644 index 0000000000..fcc8e20f1c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx @@ -0,0 +1,53 @@ +import { FieldErrors } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { serializeTextFieldValue } from '@/pages/CollectionFlowV2/components/ui/fields/TextField/helpers'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, Input, TextArea } from '@ballerine/ui'; +import { FunctionComponent, useCallback, useMemo } from 'react'; + +export type TTextFieldValueType = string | number | undefined; +export type TTextFieldOptions = { + valueType: 'integer' | 'number' | 'string'; + style: 'text' | 'textarea'; + placeholder?: string; +}; + +export const TextField: FunctionComponent< + IFieldComponentProps +> = ({ options = {} as TTextFieldOptions, definition, stack, fieldProps }) => { + const { valueType = 'string', style = 'text', placeholder } = options; + const { onChange, onBlur, value, disabled } = fieldProps; + const testId = useMemo(() => createTestId(definition, stack), [definition, stack]); + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const serializedValue = serializeTextFieldValue(event.target.value, valueType); + + onChange(serializedValue); + }, + [onChange, valueType], + ); + + const inputProps = { + value: value || '', + placeholder, + disabled, + onChange: handleChange, + onBlur, + }; + + return ( + + {style === 'textarea' ? ( + , +})); + +vi.mock('@/components/atoms/Input', () => ({ + Input: ({ children, ...props }: any) => {children}, +})); + +vi.mock('../../hooks/external', () => ({ + useField: vi.fn(), +})); + +vi.mock('../FieldList/providers/StackProvider', () => ({ + useStack: vi.fn(), +})); + +vi.mock('@/components/organisms/Renderer', () => ({ + createTestId: vi.fn(), +})); + +vi.mock('../../layouts/FieldLayout', () => ({ + FieldLayout: ({ children }: any) =>
{children}
, +})); + +vi.mock('../../layouts/FieldErrors', () => ({ + FieldErrors: () => null, +})); + +describe('TextField', () => { + const mockStack = [0]; + const mockElement = { + id: 'test-field', + params: { + valueType: 'string', + style: 'text', + placeholder: 'Enter text', + }, + } as IFormElement; + + const mockFieldProps = { + value: '', + onChange: vi.fn(), + onBlur: vi.fn(), + disabled: false, + touched: false, + } as ReturnType; + + beforeEach(() => { + cleanup(); + vi.clearAllMocks(); + vi.mocked(useStack).mockReturnValue({ stack: mockStack }); + vi.mocked(useField).mockReturnValue(mockFieldProps); + vi.mocked(createTestId).mockReturnValue('test-id'); + }); + + it('should render Input component when style is text', () => { + render(); + + expect(screen.getByTestId('test-id')).toBeInTheDocument(); + }); + + it('should render TextArea component when style is textarea', () => { + const textAreaElement = { + ...mockElement, + params: { ...mockElement.params, style: 'textarea' }, + } as unknown as IFormElement; + + render(); + + expect(screen.getByTestId('test-id')).toHaveProperty('tagName', 'TEXTAREA'); + }); + + it('should set number input type when valueType is number', () => { + const numberElement = { + ...mockElement, + params: { ...mockElement.params, valueType: 'number' }, + } as unknown as IFormElement; + + render(); + + expect(screen.getByTestId('test-id')).toHaveAttribute('type', 'number'); + }); + + it('should handle value changes', async () => { + const user = userEvent.setup(); + render(); + + const input = screen.getByTestId('test-id'); + await user.type(input, 'test value'); + + expect(mockFieldProps.onChange).toHaveBeenCalled(); + }); + + it('should handle blur events', async () => { + const user = userEvent.setup(); + render(); + + const input = screen.getByTestId('test-id'); + await user.click(input); + await user.tab(); + + expect(mockFieldProps.onBlur).toHaveBeenCalled(); + }); + + it('should respect disabled state', () => { + vi.mocked(useField).mockReturnValue({ + ...mockFieldProps, + disabled: true, + }); + + render(); + + expect(screen.getByTestId('test-id')).toBeDisabled(); + }); + + it('should display placeholder text', () => { + render(); + + expect(screen.getByTestId('test-id')).toHaveAttribute('placeholder', 'Enter text'); + }); + + it('should convert empty string to empty string for string type', async () => { + const user = userEvent.setup(); + render(); + + const input = screen.getByTestId('test-id'); + await user.clear(input); + + expect(input).toHaveValue(''); + }); + + it('should use default params when none provided', () => { + const elementWithoutParams = { + id: 'test-field', + } as unknown as IFormElement; + + render(); + + const input = screen.getByTestId('test-id'); + expect(input).toHaveAttribute('type', 'text'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/helpers.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/helpers.ts index 25e5f46c85..e2d766a0c6 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/helpers.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/helpers.ts @@ -1,15 +1,12 @@ -import { - TTextFieldOptions, - TTextFieldValueType, -} from '@/pages/CollectionFlowV2/components/ui/fields/TextField/TextField'; +import { ITextFieldParams } from './TextField'; export const serializeTextFieldValue = ( value: unknown, - valueType: TTextFieldOptions['valueType'], -): TTextFieldValueType => { + valueType: ITextFieldParams['valueType'], +) => { if (valueType === 'integer' || valueType === 'number') { return value ? Number(value) : undefined; } - return value as TTextFieldValueType; + return value; }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/helpers.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/helpers.unit.test.ts new file mode 100644 index 0000000000..7246ffca1b --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/helpers.unit.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { serializeTextFieldValue } from './helpers'; + +describe('serializeTextFieldValue', () => { + describe('when valueType is integer', () => { + it('should convert value to number', () => { + expect(serializeTextFieldValue('123', 'integer')).toBe(123); + }); + + it('should return undefined for empty value', () => { + expect(serializeTextFieldValue('', 'integer')).toBeUndefined(); + }); + }); + + describe('when valueType is number', () => { + it('should convert value to number', () => { + expect(serializeTextFieldValue('123.45', 'number')).toBe(123.45); + }); + + it('should return undefined for empty value', () => { + expect(serializeTextFieldValue('', 'number')).toBeUndefined(); + }); + }); + + describe('when valueType is string', () => { + it('should return value as is', () => { + expect(serializeTextFieldValue('test', 'string')).toBe('test'); + }); + + it('should return empty string as is', () => { + expect(serializeTextFieldValue('', 'string')).toBe(''); + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts new file mode 100644 index 0000000000..6ac7454e90 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts @@ -0,0 +1,7 @@ +export * from './AutocompleteField'; +export * from './CheckboxField'; +export * from './CheckboxList'; +export * from './DateField'; +export * from './FieldList'; +export * from './MultiselectField'; +export * from './TextField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts index 8acc1cb2bf..5445253e24 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts @@ -6,7 +6,10 @@ import { IFormElement } from '../../../types'; import { useElementId } from '../useElementId'; import { useValueDestination } from '../useValueDestination'; -export const useField = (element: IFormElement, stack: TDeepthLevelStack = []) => { +export const useField = ( + element: IFormElement, + stack: TDeepthLevelStack = [], +) => { const fieldId = useElementId(element, stack); const valueDestination = useValueDestination(element, stack); const { fieldHelpers, values } = useDynamicForm(); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldErrors/FieldErrors.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldErrors/FieldErrors.tsx index b8b1556110..7c775a45dd 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldErrors/FieldErrors.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldErrors/FieldErrors.tsx @@ -1,25 +1,24 @@ -import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; -import { useValidatedInput } from '@/components/providers/Validator/hooks/useValidatedInput'; -import { useValidator } from '@/components/providers/Validator/hooks/useValidator'; -import { UIElementV2 } from '@/components/providers/Validator/types'; -import { useStack } from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider'; -import { useTouched } from '@/pages/CollectionFlowV2/hocs/withConnectedField'; -import { useUIElement } from '@/pages/CollectionFlowV2/hooks/useUIElement'; -import { ErrorsList } from '@ballerine/ui'; -import { FunctionComponent } from 'react'; +import { ErrorsList } from '@/components/molecules/ErrorsList'; +import { FunctionComponent, useMemo } from 'react'; +import { useValidator } from '../../../Validator'; +import { useElement } from '../../hooks/external'; +import { IFormElement } from '../../types'; export interface IFieldErrorsProps { - definition: UIElementV2; + element: IFormElement; stack?: number[]; } -export const FieldErrors: FunctionComponent = ({ definition }) => { - const { payload } = useStateManagerContext(); - const { stack } = useStack(); - const uiElement = useUIElement(definition, payload, stack); - const { isTouched } = useTouched(uiElement); - const errors = useValidatedInput(uiElement); +export const FieldErrors: FunctionComponent = ({ element, stack }) => { + const { id } = useElement(element, stack); const { errors: _validationErrors } = useValidator(); - return isTouched && ; + const fieldErrors = useMemo(() => { + return _validationErrors + .filter(error => error.id === id) + .map(error => error.message) + .flat(); + }, [_validationErrors, id]); + + return ; }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldErrors/FieldErrors.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldErrors/FieldErrors.unit.test.tsx new file mode 100644 index 0000000000..a68bd96acb --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldErrors/FieldErrors.unit.test.tsx @@ -0,0 +1,112 @@ +import { ErrorsList } from '@/components/molecules/ErrorsList'; +import { cleanup, render } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { IValidationError, useValidator } from '../../../Validator'; +import { IValidatorContext } from '../../../Validator/context'; +import { useElement } from '../../hooks/external'; +import { IFormElement } from '../../types'; +import { FieldErrors } from './FieldErrors'; + +// Mock dependencies +vi.mock('@/components/molecules/ErrorsList', () => ({ + ErrorsList: vi.fn(({ errors }) =>
{errors.join(', ')}
), +})); + +vi.mock('../../../Validator', () => ({ + useValidator: vi.fn(), +})); + +vi.mock('../../hooks/external', () => ({ + useElement: vi.fn(), +})); + +describe('FieldErrors', () => { + const mockElement = { + id: 'test-field', + type: 'text', + } as unknown as IFormElement; + + beforeEach(() => { + cleanup(); + vi.clearAllMocks(); + + // Default mock implementations + vi.mocked(useElement).mockReturnValue({ id: 'test-field' } as ReturnType); + vi.mocked(useValidator).mockReturnValue({ + errors: [], + } as unknown as IValidatorContext); + }); + + it('renders ErrorsList component', () => { + render(); + expect(ErrorsList).toHaveBeenCalled(); + }); + + it('filters errors by field id', () => { + vi.mocked(useValidator).mockReturnValue({ + errors: [ + { id: 'test-field', message: ['Error 1'] }, + { id: 'other-field', message: ['Error 2'] }, + { id: 'test-field', message: ['Error 3'] }, + ] as unknown as IValidationError[], + } as unknown as IValidatorContext); + + render(); + + expect(ErrorsList).toHaveBeenCalledWith( + expect.objectContaining({ + errors: ['Error 1', 'Error 3'], + }), + expect.anything(), + ); + }); + + it('handles array of error messages', () => { + vi.mocked(useValidator).mockReturnValue({ + errors: [ + { id: 'test-field', message: ['Error 1a', 'Error 1b'] }, + { id: 'test-field', message: ['Error 2'] }, + ] as unknown as IValidationError[], + } as unknown as IValidatorContext); + + render(); + + expect(ErrorsList).toHaveBeenCalledWith( + expect.objectContaining({ + errors: ['Error 1a', 'Error 1b', 'Error 2'], + }), + expect.anything(), + ); + }); + + it('passes empty array when no errors match field id', () => { + vi.mocked(useValidator).mockReturnValue({ + errors: [ + { id: 'other-field', message: ['Error 1'] }, + { id: 'another-field', message: ['Error 2'] }, + ] as unknown as IValidationError[], + } as unknown as IValidatorContext); + + render(); + + expect(ErrorsList).toHaveBeenCalledWith( + expect.objectContaining({ + errors: [], + }), + expect.anything(), + ); + }); + + it('uses stack for element id when provided', () => { + const stack = [1, 2, 3]; + vi.mocked(useElement).mockReturnValue({ + id: 'test-field-1.2.3', + originId: 'test-field', + hidden: false, + } as ReturnType); + + render(); + + expect(useElement).toHaveBeenCalledWith(mockElement, stack); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx index fabc24de06..ca527b8c5a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx @@ -1,4 +1,5 @@ -import { ctw, Label } from '@ballerine/ui'; +import { ctw } from '@/common'; +import { Label } from '@/components/atoms'; import { useDynamicForm } from '../../context'; import { useStack } from '../../fields/FieldList/providers/StackProvider'; import { useElement } from '../../hooks/external'; @@ -9,13 +10,15 @@ export interface IFieldLayoutBaseParams { label?: string; } -export const FieldLayout: TDynamicFormField = ({ element, children }) => { +export const FieldLayout: TDynamicFormField = ({ element, children }) => { const { values } = useDynamicForm(); const { stack } = useStack(); - const { id } = useElement(element, stack); + const { id, hidden } = useElement(element, stack); const { label } = element.params || {}; const isRequired = useRequired(element, values); + if (hidden) return null; + return (
({ })); vi.mock('../../hooks/external', () => ({ - useElement: vi.fn(element => ({ id: element.id })), + useElement: vi.fn(element => ({ id: element.id, hidden: false })), })); vi.mock('../../hooks/external/useRequired', () => ({ @@ -27,6 +28,12 @@ vi.mock('../../hooks/external/useRequired', () => ({ })); describe('FieldLayout', () => { + beforeEach(() => { + cleanup(); + vi.clearAllMocks(); + vi.restoreAllMocks(); + }); + const mockElement = { id: 'test-field', params: { @@ -100,4 +107,22 @@ describe('FieldLayout', () => { const label = screen.getByText('Test Label (optional)'); expect(label).toHaveAttribute('id', 'test-field-label'); }); + + it('should not render anything when hidden is true', () => { + vi.mocked(useElement).mockReturnValue({ id: 'test-field', hidden: true } as any); + vi.mocked(useRequired).mockReturnValue(false); + + render(); + + expect(screen.queryByTestId('test-field-field-layout')).not.toBeInTheDocument(); + }); + + it('should apply gap class when label exists', () => { + vi.mocked(useElement).mockReturnValue({ id: 'test-field', hidden: false } as any); + vi.mocked(useRequired).mockReturnValue(false); + + render(); + + expect(screen.getByTestId('test-field-field-layout')).toHaveClass('flex flex-col'); + }); }); diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index d41e73e2e1..5f95b7a79c 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "./dist", "baseUrl": ".", + "rootDir": "./src", "jsx": "react-jsx", "paths": { "@/*": ["./src/*"] diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 84d6ca49c3..6fa92905f1 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -41,6 +41,17 @@ export default defineConfig({ environment: 'jsdom', setupFiles: ['./src/setupTests.ts'], css: true, + environmentOptions: { + jsdom: { + resources: 'usable', + features: { + // Disable features you don't need + FetchExternalResources: false, + ProcessExternalResources: false, + SkipExternalResources: true, + }, + }, + }, }, build: { outDir: 'dist', From e74241b8bd40c1e7a74e4e50e74ffe13c966d8b1 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Tue, 17 Dec 2024 11:15:50 +0200 Subject: [PATCH 16/54] fix: fixed build & tests --- .../controls/SubmitButton/SubmitButton.tsx | 5 +-- .../SubmitButton/SubmitButton.unit.test.tsx | 15 +++++--- .../AutocompleteField/AutocompleteField.tsx | 2 +- .../AutocompleteField.unit.test.tsx | 9 +++-- .../fields/TextField/TextField.tsx | 2 +- .../hooks/external/useElement/useElement.ts | 5 ++- .../external/useElementId/useElementId.ts | 2 +- .../FieldLayout/FieldLayout.unit.test.tsx | 6 +++- packages/ui/src/setupTests.ts | 2 +- pnpm-lock.yaml | 36 +++++++++++++++++++ 10 files changed, 69 insertions(+), 15 deletions(-) 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 b8faa13bc1..b977e71623 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 @@ -1,8 +1,9 @@ -import { Button, useElement } from '@ballerine/ui'; +import { Button } from '@/components/atoms'; import { useMemo } from 'react'; import { useValidator } from '../../../Validator'; import { useDynamicForm } from '../../context'; -import { useField } from '../../hooks/external'; +import { useElement } from '../../hooks/external/useElement'; +import { useField } from '../../hooks/external/useField'; import { TBaseFormElements, TDynamicFormElement } from '../../types'; export interface ISubmitButtonParams { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx index d451d5034b..fcb945712a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx @@ -1,19 +1,26 @@ -import { Button } from '@ballerine/ui'; +import { Button } from '@/components/atoms'; import '@testing-library/jest-dom'; import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useValidator } from '../../../Validator'; import { useDynamicForm } from '../../context'; -import { useField } from '../../hooks/external'; +import { useField } from '../../hooks/external/useField'; import { IFormElement, TBaseFormElements } from '../../types'; import { ISubmitButtonParams, SubmitButton } from './SubmitButton'; -vi.mock('@ballerine/ui', () => ({ +vi.mock('@/components/atoms', () => ({ Button: vi.fn(), +})); + +vi.mock('../../hooks/external/useElement', () => ({ useElement: vi.fn().mockReturnValue({ id: 'test-id' }), })); +vi.mock('../../hooks/external/useField', () => ({ + useField: vi.fn().mockReturnValue({ disabled: false }), +})); + vi.mock('../../../Validator', () => ({ useValidator: vi.fn(), })); @@ -38,7 +45,7 @@ describe('SubmitButton', () => { beforeEach(() => { cleanup(); - vi.clearAllMocks(); + // vi.restoreAllMocks(); vi.mocked(Button).mockImplementation(({ children, ...props }) => ( diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx index bdeccbba10..1ce118c6bf 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx @@ -1,5 +1,5 @@ +import { AutocompleteInput } from '@/components/molecules'; import { createTestId } from '@/components/organisms/Renderer'; -import { AutocompleteInput } from '@ballerine/ui'; import { useField } from '../../hooks/external'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TBaseFormElements, TDynamicFormField } from '../../types'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx index 7e04eb6418..3aae146327 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx @@ -8,9 +8,9 @@ import { useStack } from '../FieldList/providers/StackProvider'; import { AutocompleteField, IAutocompleteFieldParams } from './AutocompleteField'; // Mock dependencies -vi.mock('@ballerine/ui', () => ({ +vi.mock('@/components/molecules', () => ({ AutocompleteInput: ({ children, options, ...props }: any) => ( - + ), })); @@ -127,6 +127,9 @@ describe('AutocompleteField', () => { render(); - expect(screen.getByTestId('test-id')).toHaveAttribute('options', JSON.stringify(mockOptions)); + expect(screen.getByTestId('test-id')).toHaveAttribute( + 'data-options', + JSON.stringify(mockOptions), + ); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx index 5ac993c64d..c760e0a18b 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx @@ -56,7 +56,7 @@ export const TextField: TDynamicFormField = value={value?.toString() || ''} // Ensure value is string or number /> )} - + ); }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts index cb224d9e3e..c0c93c7c55 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts @@ -5,7 +5,10 @@ import { useDynamicForm } from '../../../context'; import { IFormElement } from '../../../types'; import { useElementId } from '../useElementId'; -export const useElement = (element: IFormElement, stack: TDeepthLevelStack = []) => { +export const useElement = ( + element: IFormElement, + stack: TDeepthLevelStack = [], +) => { const { values } = useDynamicForm(); const hiddenRulesResult = useRuleEngine(values, { rules: element.hidden, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.ts index 98864639cf..4dabc5ad0c 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElementId/useElementId.ts @@ -3,7 +3,7 @@ import { formatId } from '@/components/organisms/Form/Validator/utils/format-id' import { useMemo } from 'react'; import { IFormElement } from '../../../types'; -export const useElementId = (element: IFormElement, stack: TDeepthLevelStack = []) => { +export const useElementId = (element: IFormElement, stack: TDeepthLevelStack = []) => { const formattedId = useMemo(() => formatId(element.id, stack), [element.id, stack]); return formattedId; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx index a45014bcc0..54dc08b965 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx @@ -6,8 +6,12 @@ import { IFormElement } from '../../types'; import { FieldLayout } from './FieldLayout'; // Mock dependencies -vi.mock('@ballerine/ui', () => ({ + +vi.mock('@/common', () => ({ ctw: vi.fn((base, conditionals) => base), +})); + +vi.mock('@/components/atoms', () => ({ Label: ({ children, ...props }: any) => , })); diff --git a/packages/ui/src/setupTests.ts b/packages/ui/src/setupTests.ts index a2fc0080b4..b2596a8a6d 100644 --- a/packages/ui/src/setupTests.ts +++ b/packages/ui/src/setupTests.ts @@ -3,7 +3,7 @@ import matchers from '@testing-library/jest-dom/matchers'; import { cleanup } from '@testing-library/react'; import { afterEach, expect } from 'vitest'; -if(matchers) { +if (matchers) { // Extend Vitest's expect with jest-dom matchers expect.extend(matchers); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8eb47785f6..5102a75f28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1884,9 +1884,15 @@ importers: '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 '@testing-library/react': specifier: ^13.3.0 version: 13.4.0(react-dom@18.2.0)(react@18.2.0) + '@testing-library/user-event': + specifier: ^14.5.2 + version: 14.5.2(@testing-library/dom@10.4.0) '@types/json-logic-js': specifier: ^2.0.1 version: 2.0.5 @@ -3078,6 +3084,10 @@ packages: resolution: {integrity: sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==} dev: true + /@adobe/css-tools@4.4.1: + resolution: {integrity: sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==} + dev: true + /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -19068,6 +19078,19 @@ packages: vitest: 0.34.6(jsdom@20.0.3) dev: true + /@testing-library/jest-dom@6.6.3: + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + dependencies: + '@adobe/css-tools': 4.4.1 + aria-query: 5.3.0 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + dev: true + /@testing-library/react@13.4.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==} engines: {node: '>=12'} @@ -19111,6 +19134,15 @@ packages: '@testing-library/dom': 9.3.3 dev: true + /@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0): + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + dependencies: + '@testing-library/dom': 10.4.0 + dev: true + /@tiptap/core@2.9.1(@tiptap/pm@2.9.1): resolution: {integrity: sha512-tifnLL/ARzQ6/FGEJjVwj9UT3v+pENdWHdk9x6F3X0mB1y0SeCjV21wpFLYESzwNdBPAj8NMp8Behv7dBnhIfw==} peerDependencies: @@ -25970,6 +26002,10 @@ packages: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} dev: true + /dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dev: true + /dom-css@2.1.0: resolution: {integrity: sha512-w9kU7FAbaSh3QKijL6n59ofAhkkmMJ31GclJIz/vyQdjogfyxcB6Zf8CZyibOERI5o0Hxz30VmJS7+7r5fEj2Q==} dependencies: From a550b9645682b2df02325a2c5b07040ea9bca198 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Tue, 17 Dec 2024 15:46:07 +0200 Subject: [PATCH 17/54] feat: implemented events & fixed tests --- .../AutocompleteInput/AutocompleteInput.tsx | 3 + .../DatePickerInput/DatePickerInput.tsx | 3 + .../inputs/MultiSelect/MultiSelect.tsx | 7 +- .../Form/DynamicForm/DynamicForm.tsx | 6 +- .../DynamicForm/DynamicForm.unit.test.tsx | 171 ++++++++++++++++++ .../useDynamicForm.unit.test.ts | 6 +- .../Form/DynamicForm/context/types.ts | 6 + .../SubmitButton/SubmitButton.unit.test.tsx | 21 +-- .../AutocompleteField/AutocompleteField.tsx | 8 +- .../AutocompleteField.unit.test.tsx | 31 +++- .../fields/CheckboxField/CheckboxField.tsx | 15 +- .../CheckboxField/CheckboxField.unit.test.tsx | 66 ++++--- .../fields/CheckboxList/CheckboxList.tsx | 4 +- .../CheckboxList/CheckboxList.unit.test.tsx | 55 +++++- .../fields/DateField/DateField.tsx | 5 +- .../fields/DateField/DateField.unit.test.tsx | 35 +++- .../StackProvider/StackProvider.unit.test.tsx | 7 +- .../MultiselectField/MultiselectField.tsx | 7 +- .../MultiselectField.unit.test.tsx | 81 ++++++++- .../fields/TextField/TextField.tsx | 3 +- .../fields/TextField/TextField.unit.test.tsx | 28 +++ .../hooks/external/useElement/useElement.ts | 7 + .../useElement/useElement.unit.test.ts | 14 +- .../hooks/external/useField/useField.ts | 16 +- .../external/useField/useField.unit.test.ts | 57 +++++- .../hooks/internal/useCallbacks/index.ts | 1 + .../useCallbacks/useCallabacks.unit.test.ts | 34 ++++ .../internal/useCallbacks/useCallbacks.ts | 7 + .../hooks/internal/useEvents/index.ts | 1 + .../hooks/internal/useEvents/types/index.ts | 21 +++ .../hooks/internal/useEvents/useEvents.ts | 45 +++++ .../internal/useEvents/useEvents.unit.test.ts | 115 ++++++++++++ .../hooks/internal/useMount/index.ts | 1 + .../hooks/internal/useMount/useMount.ts | 13 ++ .../internal/useMount/useMount.unit.test.ts | 35 ++++ .../hooks/internal/useUnmount/index.ts | 1 + .../hooks/internal/useUnmount/useUnmount.ts | 13 ++ .../useUnmount/useUnmount.unit.test.ts | 40 ++++ .../organisms/Form/DynamicForm/types/index.ts | 2 + .../useValidate/useAsyncValidate.unit.test.ts | 40 ++-- .../useValidate/useSyncValidate.unit.test.ts | 20 +- .../useValidatorRef.unit.test.ts | 5 + packages/ui/src/setupTests.ts | 5 +- 43 files changed, 954 insertions(+), 107 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallabacks.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallbacks.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/useMount.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/useMount.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/useUnmount.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/useUnmount.unit.test.ts diff --git a/packages/ui/src/components/molecules/inputs/AutocompleteInput/AutocompleteInput.tsx b/packages/ui/src/components/molecules/inputs/AutocompleteInput/AutocompleteInput.tsx index bbcc66a5e7..b4ebeaa41e 100644 --- a/packages/ui/src/components/molecules/inputs/AutocompleteInput/AutocompleteInput.tsx +++ b/packages/ui/src/components/molecules/inputs/AutocompleteInput/AutocompleteInput.tsx @@ -25,6 +25,7 @@ export interface AutocompleteInputProps { textInputClassName?: string; onChange: (event: AutocompleteChangeEvent) => void; onBlur?: (event: FocusEvent) => void; + onFocus?: (event: FocusEvent) => void; } export const AutocompleteInput = ({ @@ -37,6 +38,7 @@ export const AutocompleteInput = ({ textInputClassName, onChange, onBlur, + onFocus, }: AutocompleteInputProps) => { const safeValue = useMemo(() => { if (typeof value !== 'string') { @@ -81,6 +83,7 @@ export const AutocompleteInput = ({ PaperComponent={Paper as ComponentProps['PaperComponent']} onChange={handleChange} disabled={disabled} + onFocus={onFocus} slotProps={{ paper: { className: 'mt-2 mb-2 w-full', diff --git a/packages/ui/src/components/molecules/inputs/DatePickerInput/DatePickerInput.tsx b/packages/ui/src/components/molecules/inputs/DatePickerInput/DatePickerInput.tsx index e4a02cb337..eacac83ef8 100644 --- a/packages/ui/src/components/molecules/inputs/DatePickerInput/DatePickerInput.tsx +++ b/packages/ui/src/components/molecules/inputs/DatePickerInput/DatePickerInput.tsx @@ -36,6 +36,7 @@ export interface DatePickerProps { textInputClassName?: string; onChange: (event: DatePickerChangeEvent) => void; onBlur?: (event: FocusEvent) => void; + onFocus?: (event: FocusEvent) => void; } export const DatePickerInput = ({ @@ -47,6 +48,7 @@ export const DatePickerInput = ({ textInputClassName, onChange, onBlur, + onFocus, }: DatePickerProps) => { const { outputValueFormat = 'iso', @@ -124,6 +126,7 @@ export const DatePickerInput = ({ onFocus={e => { setFocused(true); props.onFocus && props.onFocus(e); + onFocus && onFocus(e); }} onBlur={e => { setFocused(false); diff --git a/packages/ui/src/components/molecules/inputs/MultiSelect/MultiSelect.tsx b/packages/ui/src/components/molecules/inputs/MultiSelect/MultiSelect.tsx index 45a37c9080..328a446119 100644 --- a/packages/ui/src/components/molecules/inputs/MultiSelect/MultiSelect.tsx +++ b/packages/ui/src/components/molecules/inputs/MultiSelect/MultiSelect.tsx @@ -30,6 +30,7 @@ export interface MultiSelectProps { renderSelected: MultiSelectSelectedItemRenderer; onChange: (selected: MultiSelectValue[], inputName: string) => void; onBlur?: (event: FocusEvent) => void; + onFocus?: (event: FocusEvent) => void; } export const MultiSelect = ({ @@ -43,6 +44,7 @@ export const MultiSelect = ({ renderSelected, onChange, onBlur, + onFocus, }: MultiSelectProps) => { const inputRef = useRef(null); const [open, setOpen] = useState(false); @@ -174,7 +176,10 @@ export const MultiSelect = ({ placeholder={searchPlaceholder} style={{ border: 'none' }} className={ctw('placeholder:text-muted-foreground h-6', textInputClassName)} - onFocus={() => setOpen(true)} + onFocus={event => { + setOpen(true); + onFocus?.(event); + }} onBlur={onBlur} data-testid={testId ? `${testId}-search-input` : undefined} /> diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx index a1aefca7b2..84b616d94a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -18,6 +18,7 @@ export const DynamicFormV2: FunctionComponent = ({ onChange, onFieldChange, onSubmit, + onEvent, }) => { const validationSchema = useValidationSchema(elements); const valuesApi = useValues({ @@ -36,8 +37,11 @@ export const DynamicFormV2: FunctionComponent = ({ submit, fieldHelpers, elementsMap, + callbacks: { + onEvent, + }, }), - [touchedApi.touched, valuesApi.values, submit, fieldHelpers, elementsMap], + [touchedApi.touched, valuesApi.values, submit, fieldHelpers, elementsMap, onEvent], ); return ( 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 new file mode 100644 index 0000000000..4d984ff72c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.unit.test.tsx @@ -0,0 +1,171 @@ +import { cleanup, render } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Renderer } from '../../Renderer'; +import { ValidatorProvider } from '../Validator'; +import { DynamicFormContext } from './context'; +import { DynamicFormV2 } from './DynamicForm'; +import { useSubmit } from './hooks/external'; +import { useFieldHelpers } from './hooks/internal/useFieldHelpers'; +import { useTouched } from './hooks/internal/useTouched'; +import { useValidationSchema } from './hooks/internal/useValidationSchema'; +import { useValues } from './hooks/internal/useValues'; +import { ICommonFieldParams, IDynamicFormProps, IFormElement, TBaseFormElements } from './types'; + +// Mock dependencies +vi.mock('../../Renderer'); + +vi.mock('../Validator'); + +vi.mock('./hooks/external/useSubmit'); + +vi.mock('./hooks/internal/useFieldHelpers'); + +vi.mock('./hooks/internal/useTouched'); + +vi.mock('./hooks/internal/useValidationSchema'); + +vi.mock('./hooks/internal/useValues'); + +vi.mock('./context', () => ({ + DynamicFormContext: { + Provider: vi.fn(({ children, value }: any) => { + return
{children}
; + }), + }, +})); + +describe('DynamicFormV2', () => { + beforeEach(() => { + cleanup(); + vi.restoreAllMocks(); + + vi.mocked(Renderer).mockImplementation(({ children }: any) => { + return
{children}
; + }); + vi.mocked(ValidatorProvider).mockImplementation(({ children }: any) => { + return
{children}
; + }); + + vi.mocked(useTouched).mockReturnValue({ + touched: {}, + setTouched: vi.fn(), + setFieldTouched: vi.fn(), + touchAllFields: vi.fn(), + } as any); + vi.mocked(useFieldHelpers).mockReturnValue({ + getTouched: vi.fn(), + getValue: vi.fn(), + setTouched: vi.fn(), + setValue: vi.fn(), + } as any); + vi.mocked(useSubmit).mockReturnValue({ submit: vi.fn() } as any); + vi.mocked(useValidationSchema).mockReturnValue([] as any); + vi.mocked(useValues).mockReturnValue({ + values: {}, + setValues: vi.fn(), + setFieldValue: vi.fn(), + } as any); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + const mockProps = { + elements: [], + values: {}, + validationParams: {}, + elementsMap: {}, + onChange: vi.fn(), + onFieldChange: vi.fn(), + onSubmit: vi.fn(), + onEvent: vi.fn(), + } as unknown as IDynamicFormProps; + + it('should render without crashing', () => { + render(); + }); + + it('should pass elements to useValidationSchema', () => { + const elements = [{ id: 'test', element: 'text' }] as unknown as Array< + IFormElement + >; + render(); + expect(useValidationSchema).toHaveBeenCalledWith(elements); + }); + + it('should pass correct props to useValues', () => { + render(); + expect(useValues).toHaveBeenCalledWith({ + values: mockProps.values, + onChange: mockProps.onChange, + onFieldChange: mockProps.onFieldChange, + }); + }); + + it('should pass correct props to useTouched', () => { + render(); + expect(useTouched).toHaveBeenCalledWith(mockProps.elements, mockProps.values); + }); + + it('should pass correct props to useFieldHelpers', () => { + render(); + expect(useFieldHelpers).toHaveBeenCalledWith({ + valuesApi: useValues({ + values: mockProps.values, + onChange: mockProps.onChange, + onFieldChange: mockProps.onFieldChange, + }), + touchedApi: useTouched(mockProps.elements, mockProps.values), + }); + }); + + it('should pass correct props to useSubmit', () => { + render(); + expect(useSubmit).toHaveBeenCalledWith({ + values: mockProps.values, + onSubmit: mockProps.onSubmit, + }); + }); + it('should pass context to DynamicFormContext.Provider', () => { + const touchedMock = { + touched: { field1: true }, + setTouched: vi.fn(), + setFieldTouched: vi.fn(), + touchAllFields: vi.fn(), + }; + const valuesMock = { + values: { field1: 'value1' }, + setValues: vi.fn(), + setFieldValue: vi.fn(), + }; + const submitMock = { submit: vi.fn() }; + const fieldHelpersMock = { + getTouched: vi.fn(), + getValue: vi.fn(), + setTouched: vi.fn(), + setValue: vi.fn(), + }; + + vi.mocked(useTouched).mockReturnValue(touchedMock); + vi.mocked(useValues).mockReturnValue(valuesMock); + vi.mocked(useSubmit).mockReturnValue(submitMock); + vi.mocked(useFieldHelpers).mockReturnValue(fieldHelpersMock); + + render(); + + // Get the actual props passed to DynamicFormContext.Provider + const providerProps = vi.mocked(DynamicFormContext.Provider).mock.calls[0]?.[0]; + + expect(providerProps?.value).toEqual({ + touched: touchedMock.touched, + values: valuesMock.values, + submit: submitMock.submit, + fieldHelpers: fieldHelpersMock, + elementsMap: mockProps.elementsMap, + callbacks: { + onEvent: mockProps.onEvent, + }, + }); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts index 9e2abc9739..aff3acdb58 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/hooks/useDynamicForm/useDynamicForm.unit.test.ts @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react'; import { useContext } from 'react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { DynamicFormContext } from '../../dynamic-form.context'; import { useDynamicForm } from './useDynamicForm'; @@ -21,6 +21,10 @@ describe('useDynamicForm', () => { vi.mocked(useContext).mockReturnValue(mockContextValue); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should call useContext with DynamicFormContext', () => { renderHook(() => useDynamicForm()); 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 5d3e76d3d5..e34611e7b0 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts @@ -1,11 +1,17 @@ +import { IFormEventElement, TElementEvent } from '../hooks/internal/useEvents/types'; import { IFieldHelpers } from '../hooks/internal/useFieldHelpers/types'; import { ITouchedState } from '../hooks/internal/useTouched'; import { TElementsMap } from '../types'; +export interface IDynamicFormCallbacks { + onEvent?: (eventName: TElementEvent, element: IFormEventElement) => void; +} + export interface IDynamicFormContext { values: TValues; touched: ITouchedState; elementsMap: TElementsMap; fieldHelpers: IFieldHelpers; submit: () => void; + callbacks: IDynamicFormCallbacks; } diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx index fcb945712a..ad1132a64e 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx @@ -2,9 +2,10 @@ import { Button } from '@/components/atoms'; import '@testing-library/jest-dom'; import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useValidator } from '../../../Validator'; import { useDynamicForm } from '../../context'; +import { useElement } from '../../hooks/external'; import { useField } from '../../hooks/external/useField'; import { IFormElement, TBaseFormElements } from '../../types'; import { ISubmitButtonParams, SubmitButton } from './SubmitButton'; @@ -13,13 +14,7 @@ vi.mock('@/components/atoms', () => ({ Button: vi.fn(), })); -vi.mock('../../hooks/external/useElement', () => ({ - useElement: vi.fn().mockReturnValue({ id: 'test-id' }), -})); - -vi.mock('../../hooks/external/useField', () => ({ - useField: vi.fn().mockReturnValue({ disabled: false }), -})); +vi.mock('../../hooks/external/useElement'); vi.mock('../../../Validator', () => ({ useValidator: vi.fn(), @@ -29,9 +24,7 @@ vi.mock('../../context', () => ({ useDynamicForm: vi.fn(), })); -vi.mock('../../hooks/external', () => ({ - useField: vi.fn(), -})); +vi.mock('../../hooks/external/useField'); describe('SubmitButton', () => { const mockElement = { @@ -54,6 +47,12 @@ describe('SubmitButton', () => { vi.mocked(useField).mockReturnValue({ disabled: false } as any); vi.mocked(useDynamicForm).mockReturnValue({ submit: mockSubmit } as any); vi.mocked(useValidator).mockReturnValue({ isValid: true } as any); + vi.mocked(useElement).mockReturnValue({ id: 'test-id' } as any); + vi.mocked(useField).mockReturnValue({ disabled: false } as any); + }); + + afterEach(() => { + vi.restoreAllMocks(); }); it('should render button with default text', () => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx index 1ce118c6bf..5a8fe5b570 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx @@ -1,6 +1,6 @@ import { AutocompleteInput } from '@/components/molecules'; import { createTestId } from '@/components/organisms/Renderer'; -import { useField } from '../../hooks/external'; +import { useField } from '../../hooks/external/useField'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TBaseFormElements, TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; @@ -20,7 +20,10 @@ export const AutocompleteField: TDynamicFormField { const { params } = element; const { stack } = useStack(); - const { value, onChange, onBlur, disabled } = useField(element, stack); + const { value, onChange, onBlur, onFocus, disabled } = useField( + element, + stack, + ); const { options = [], placeholder = '' } = params || {}; return ( @@ -33,6 +36,7 @@ export const AutocompleteField: TDynamicFormField onChange(event.target.value || '')} onBlur={onBlur} + onFocus={onFocus} />
); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx index 3aae146327..2a846cf60b 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx @@ -2,19 +2,28 @@ import { createTestId } from '@/components/organisms/Renderer'; import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useField } from '../../hooks/external'; +import { useField } from '../../hooks/external/useField'; import { IFormElement, TBaseFormElements } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { AutocompleteField, IAutocompleteFieldParams } from './AutocompleteField'; // Mock dependencies vi.mock('@/components/molecules', () => ({ - AutocompleteInput: ({ children, options, ...props }: any) => ( - + AutocompleteInput: ({ children, options, onFocus, ...props }: any) => ( + { + console.log('FOCUS'); + onFocus?.(e); + }} + data-options={JSON.stringify(options)} + type="text" + tabIndex={0} + /> ), })); -vi.mock('../../hooks/external', () => ({ +vi.mock('../../hooks/external/useField', () => ({ useField: vi.fn(), })); @@ -49,6 +58,7 @@ describe('AutocompleteField', () => { value: '', onChange: vi.fn(), onBlur: vi.fn(), + onFocus: vi.fn(), disabled: false, touched: false, } as ReturnType; @@ -95,6 +105,19 @@ describe('AutocompleteField', () => { expect(mockFieldProps.onBlur).toHaveBeenCalled(); }); + it('should handle focus events', async () => { + const user = userEvent.setup(); + + const { container } = render(); + + const input = container.querySelector('input'); + + await user.click(input!); + console.log(input?.outerHTML); + + expect(mockFieldProps.onFocus).toHaveBeenCalled(); + }); + it('should respect disabled state', () => { vi.mocked(useField).mockReturnValue({ ...mockFieldProps, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx index 97ebd1d16f..d3a0833f0a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx @@ -5,7 +5,18 @@ import { useStack } from '../FieldList/providers/StackProvider'; export const CheckboxField: TDynamicFormField = ({ element }) => { const { stack } = useStack(); - const { value, onChange, disabled } = useField(element, stack); + const { value, onChange, onFocus, onBlur, disabled } = useField( + element, + stack, + ); - return ; + return ( + + ); }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx index edc8d1603d..1c13ac8735 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx @@ -1,7 +1,9 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; import { IFormElement, TBaseFormElements } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; import { CheckboxField } from './CheckboxField'; // Mock dependencies @@ -12,15 +14,15 @@ vi.mock('@/components/atoms', () => ({ checked={props.checked} onChange={e => props.onChange(e.target.checked)} disabled={props.disabled} + onFocus={props.onFocus} + onBlur={props.onBlur} data-testid="test-checkbox" /> )), })); vi.mock('../FieldList/providers/StackProvider', () => ({ - useStack: () => ({ - stack: [], - }), + useStack: vi.fn(), })); vi.mock('../../hooks/external/useField', () => ({ @@ -28,20 +30,25 @@ vi.mock('../../hooks/external/useField', () => ({ })); describe('CheckboxField', () => { + const mockStack = [0]; const mockElement = { id: 'test-checkbox', type: '', } as unknown as IFormElement; + const mockFieldProps = { + value: false, + onChange: vi.fn(), + onFocus: vi.fn(), + onBlur: vi.fn(), + disabled: false, + } as unknown as ReturnType; + beforeEach(() => { cleanup(); vi.clearAllMocks(); - - vi.mocked(useField).mockReturnValue({ - value: false, - onChange: vi.fn(), - disabled: false, - } as unknown as ReturnType); + vi.mocked(useStack).mockReturnValue({ stack: mockStack }); + vi.mocked(useField).mockReturnValue(mockFieldProps); }); it('renders checkbox with correct initial state', () => { @@ -53,10 +60,9 @@ describe('CheckboxField', () => { it('renders checked checkbox when value is true', () => { vi.mocked(useField).mockReturnValue({ + ...mockFieldProps, value: true, - onChange: vi.fn(), - disabled: false, - } as unknown as ReturnType); + }); render(); expect(screen.getByTestId('test-checkbox')).toBeChecked(); @@ -65,10 +71,9 @@ describe('CheckboxField', () => { it('handles onChange events', () => { const mockOnChange = vi.fn(); vi.mocked(useField).mockReturnValue({ - value: false, + ...mockFieldProps, onChange: mockOnChange, - disabled: false, - } as unknown as ReturnType); + }); render(); @@ -78,12 +83,32 @@ describe('CheckboxField', () => { expect(mockOnChange).toHaveBeenCalledWith(true); }); + it('handles focus events', async () => { + const user = userEvent.setup(); + render(); + + const checkbox = screen.getByTestId('test-checkbox'); + await user.click(checkbox); + + expect(mockFieldProps.onFocus).toHaveBeenCalled(); + }); + + it('handles blur events', async () => { + const user = userEvent.setup(); + render(); + + const checkbox = screen.getByTestId('test-checkbox'); + await user.click(checkbox); + await user.tab(); + + expect(mockFieldProps.onBlur).toHaveBeenCalled(); + }); + it('disables checkbox when disabled prop is true', () => { vi.mocked(useField).mockReturnValue({ - value: false, - onChange: vi.fn(), + ...mockFieldProps, disabled: true, - } as unknown as ReturnType); + }); render(); expect(screen.getByTestId('test-checkbox')).toBeDisabled(); @@ -91,10 +116,9 @@ describe('CheckboxField', () => { it('handles undefined value as unchecked', () => { vi.mocked(useField).mockReturnValue({ + ...mockFieldProps, value: undefined, - onChange: vi.fn(), - disabled: false, - } as unknown as ReturnType); + }); render(); expect(screen.getByTestId('test-checkbox')).not.toBeChecked(); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx index 8b1c9d2de7..b8c80d7213 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx @@ -20,7 +20,7 @@ export const CheckboxListField: TDynamicFormField { const { options = [] } = element.params || {}; const { stack } = useStack(); - const { value, onChange, disabled } = useField(element, stack); + const { value, onChange, onFocus, onBlur, disabled } = useField(element, stack); return ( @@ -36,6 +36,8 @@ export const CheckboxListField: TDynamicFormField { let val = (value as string[]) || []; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx index 5f656372d8..4806afdc82 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx @@ -1,3 +1,4 @@ +import { createTestId } from '@/components/organisms/Renderer'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external/useField'; @@ -5,9 +6,7 @@ import { IFormElement, TBaseFormElements } from '../../types'; import { CheckboxListField, ICheckboxListFieldParams } from './CheckboxList'; // Mock dependencies -vi.mock('@/components/organisms/Renderer', () => ({ - createTestId: vi.fn().mockReturnValue('test-checkbox-list'), -})); +vi.mock('@/components/organisms/Renderer'); vi.mock('../FieldList/providers/StackProvider', () => ({ useStack: () => ({ @@ -27,6 +26,9 @@ vi.mock('@/components/atoms', () => ({ onChange={e => props.onCheckedChange(e.target.checked)} data-testid={props['data-testid']} value={props.value} + onFocus={props.onFocus} + onBlur={props.onBlur} + className={props.className} /> )), })); @@ -54,9 +56,13 @@ describe('CheckboxListField', () => { cleanup(); vi.clearAllMocks(); + vi.mocked(createTestId).mockReturnValue('test-checkbox-list'); + vi.mocked(useField).mockReturnValue({ value: ['opt1'], onChange: vi.fn(), + onFocus: vi.fn(), + onBlur: vi.fn(), disabled: false, } as unknown as ReturnType); }); @@ -66,7 +72,12 @@ describe('CheckboxListField', () => { mockOptions.forEach((option, index) => { expect(screen.getByText(option.label)).toBeInTheDocument(); - expect(screen.getByTestId(`test-checkbox-list-checkbox-${index}`)).toBeInTheDocument(); + const checkbox = screen.getByTestId(`test-checkbox-list-checkbox-${index}`); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).toHaveClass('border-primary'); + expect(checkbox).toHaveClass('data-[state=checked]:bg-primary'); + expect(checkbox).toHaveClass('data-[state=checked]:text-primary-foreground'); + expect(checkbox).toHaveClass('bg-white'); }); }); @@ -74,6 +85,8 @@ describe('CheckboxListField', () => { vi.mocked(useField).mockReturnValue({ value: ['opt1', 'opt3'], onChange: vi.fn(), + onFocus: vi.fn(), + onBlur: vi.fn(), disabled: false, } as unknown as ReturnType); @@ -92,13 +105,15 @@ describe('CheckboxListField', () => { vi.mocked(useField).mockReturnValue({ value: ['opt1'], onChange: mockOnChange, + onFocus: vi.fn(), + onBlur: vi.fn(), disabled: false, } as unknown as ReturnType); render(); const checkbox1 = screen.getByTestId('test-checkbox-list-checkbox-1'); - fireEvent.click(checkbox1); // Click second checkbox + fireEvent.click(checkbox1); expect(mockOnChange).toHaveBeenCalledWith(['opt1', 'opt2']); }); @@ -108,21 +123,47 @@ describe('CheckboxListField', () => { vi.mocked(useField).mockReturnValue({ value: ['opt1', 'opt2'], onChange: mockOnChange, + onFocus: vi.fn(), + onBlur: vi.fn(), disabled: false, } as unknown as ReturnType); render(); - const checkboxes = screen.getAllByTestId('test-checkbox-list-checkbox-0'); - fireEvent.click(checkboxes[0]!); // Uncheck first checkbox + const checkbox0 = screen.getByTestId('test-checkbox-list-checkbox-0'); + fireEvent.click(checkbox0); expect(mockOnChange).toHaveBeenCalledWith(['opt2']); }); + it('handles focus and blur events', async () => { + const mockOnFocus = vi.fn(); + const mockOnBlur = vi.fn(); + + vi.mocked(useField).mockReturnValue({ + value: ['opt1'], + onChange: vi.fn(), + onFocus: mockOnFocus, + onBlur: mockOnBlur, + disabled: false, + } as unknown as ReturnType); + + render(); + + const checkbox = screen.getByTestId('test-checkbox-list-checkbox-0'); + fireEvent.focus(checkbox); + expect(mockOnFocus).toHaveBeenCalled(); + + fireEvent.blur(checkbox); + expect(mockOnBlur).toHaveBeenCalled(); + }); + it('applies disabled styling when disabled', () => { vi.mocked(useField).mockReturnValue({ value: [], onChange: vi.fn(), + onFocus: vi.fn(), + onBlur: vi.fn(), disabled: true, } as unknown as ReturnType); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx index dc449646e8..ca7b615979 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx @@ -25,7 +25,7 @@ export const DateField: TDynamicFormField = } = element.params || {}; const { stack } = useStack(); - const { value, onChange, onBlur, disabled } = useField( + const { value, onChange, onBlur, onFocus, disabled } = useField( element, stack, ); @@ -34,7 +34,7 @@ export const DateField: TDynamicFormField = (event: DatePickerChangeEvent) => { const dateValue = event.target.value; - if (dateValue === null) return onChange(null); + if (dateValue === null || dateValue === '') return onChange(null); if (!checkIfDateIsValid(dateValue)) return; @@ -56,6 +56,7 @@ export const DateField: TDynamicFormField = testId={createTestId(element, stack)} onBlur={onBlur} onChange={handleChange} + onFocus={onFocus} /> ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx index 468b0786ef..e4a373e7b0 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx @@ -1,5 +1,5 @@ import { checkIfDateIsValid } from '@/common/utils/check-if-date-is-valid'; -import { DatePickerInput } from '@/components/molecules/inputs/DatePickerInput/DatePickerInput'; +import { DatePickerInput } from '@/components/molecules'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external/useField'; @@ -27,14 +27,19 @@ vi.mock('@/components/molecules/inputs/DatePickerInput/DatePickerInput', () => ( data-testid="test-date" disabled={props.disabled} value={props.value || ''} - onInput={props.onChange} + onChange={e => { + props.onChange(e); + }} + onInput={e => { + props.onChange(e); + }} + onFocus={props.onFocus} + onBlur={props.onBlur} /> ); }), })); -vi.mock('../../hooks/external/useField', () => ({ - useField: vi.fn(), -})); +vi.mock('../../hooks/external/useField'); vi.mock('@/common/utils/check-if-date-is-valid', () => ({ checkIfDateIsValid: vi.fn(), })); @@ -44,12 +49,12 @@ describe('DateField', () => { cleanup(); vi.restoreAllMocks(); - const mockOnChange = vi.fn(); vi.mocked(useField).mockReturnValue({ value: '2023-01-01', touched: false, - onChange: mockOnChange, + onChange: vi.fn(), onBlur: vi.fn(), + onFocus: vi.fn(), disabled: false, }); }); @@ -71,13 +76,21 @@ describe('DateField', () => { it('handles null date value correctly', () => { const mockOnChange = vi.fn(); + vi.mocked(useField).mockReturnValue({ + value: '2023-01-01', + touched: false, + onChange: mockOnChange, + onBlur: vi.fn(), + onFocus: vi.fn(), + disabled: false, + }); render(); const dateInput = screen.getByTestId('test-date'); fireEvent.change(dateInput, { target: { value: null } }); - expect(mockOnChange).not.toHaveBeenCalled(); + expect(mockOnChange).toHaveBeenCalledWith(null); }); it('validates date before calling onChange', async () => { @@ -87,6 +100,7 @@ describe('DateField', () => { touched: false, onChange: mockOnChange, onBlur: vi.fn(), + onFocus: vi.fn(), disabled: false, }); @@ -94,7 +108,8 @@ describe('DateField', () => { render(); - fireEvent.input(screen.getByTestId('test-date'), { target: { value: '2023-01-01' } }); + const dateInput = screen.getByTestId('test-date'); + fireEvent.input(dateInput, { target: { value: '2023-01-01' } }); expect(checkIfDateIsValid).toHaveBeenCalledWith('2023-01-01'); expect(mockOnChange).toHaveBeenCalledWith('2023-01-01'); @@ -107,6 +122,7 @@ describe('DateField', () => { touched: false, onChange: mockOnChange, onBlur: vi.fn(), + onFocus: vi.fn(), disabled: false, }); vi.mocked(checkIfDateIsValid).mockReturnValue(false); @@ -150,6 +166,7 @@ describe('DateField', () => { touched: false, onChange: vi.fn(), onBlur: vi.fn(), + onFocus: vi.fn(), disabled: true, }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.unit.test.tsx index 450f2f2cb3..f40abc098c 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider/StackProvider.unit.test.tsx @@ -13,7 +13,7 @@ vi.mock('./context/stack-provider-context', () => ({ import { render } from '@testing-library/react'; import { useMemo } from 'react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { StackProvider } from './StackProvider'; import { StackProviderContext } from './context/stack-provider-context'; @@ -21,6 +21,11 @@ describe('StackProvider', () => { beforeEach(() => { vi.clearAllMocks(); }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should create context with provided stack', () => { const mockStack = [1, 2, 3]; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx index a026c978b4..fe0583b951 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx @@ -14,7 +14,10 @@ export const MultiselectField: TDynamicFormField { const { stack } = useStack(); - const { value, onChange, disabled } = useField(element, stack); + const { value, onChange, onBlur, onFocus, disabled } = useField( + element, + stack, + ); const renderSelected = useCallback((params: SelectedElementParams, option: MultiSelectOption) => { return ; @@ -25,6 +28,8 @@ export const MultiselectField: TDynamicFormField diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx index c5c4cdf396..61b6c5e630 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx @@ -1,10 +1,16 @@ import { MultiSelect, MultiSelectOption } from '@/components/molecules'; -import { render, screen } from '@testing-library/react'; +import { SelectedElementParams } from '@/components/molecules/inputs/MultiSelect/types'; +import { fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; import { IFormElement, TBaseFormElements } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { IMultiselectFieldParams, MultiselectField } from './MultiselectField'; +import { MultiselectfieldSelectedItem } from './MultiselectFieldSelectedItem'; + +vi.mock('./MultiselectFieldSelectedItem', () => ({ + MultiselectfieldSelectedItem: vi.fn(() =>
), +})); vi.mock('@/components/molecules', () => ({ MultiSelect: vi.fn(props => ( @@ -13,7 +19,14 @@ vi.mock('@/components/molecules', () => ({ data-testid="multiselect-input" disabled={props.disabled} onChange={e => props.onChange(e.target.value, '')} + onBlur={props.onBlur} + onFocus={props.onFocus} /> + {props.value?.map((val: string, idx: number) => ( +
+ {props.renderSelected({ unselectButtonProps: {} }, { value: val, title: val })} +
+ ))}
)), })); @@ -51,6 +64,8 @@ describe('MultiselectField', () => { vi.mocked(useField).mockReturnValue({ value: ['opt1'], onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), disabled: false, } as unknown as ReturnType); }); @@ -62,9 +77,14 @@ describe('MultiselectField', () => { it('passes correct props to MultiSelect', () => { const mockOnChange = vi.fn(); + const mockOnBlur = vi.fn(); + const mockOnFocus = vi.fn(); + vi.mocked(useField).mockReturnValue({ value: ['opt1'], onChange: mockOnChange, + onBlur: mockOnBlur, + onFocus: mockOnFocus, disabled: false, } as unknown as ReturnType); @@ -75,6 +95,8 @@ describe('MultiselectField', () => { expect(multiselect.disabled).toBe(false); expect(multiselect.options).toEqual(mockOptions); expect(multiselect.onChange).toBe(mockOnChange); + expect(multiselect.onBlur).toBe(mockOnBlur); + expect(multiselect.onFocus).toBe(mockOnFocus); }); it('handles empty options gracefully', () => { @@ -93,6 +115,8 @@ describe('MultiselectField', () => { vi.mocked(useField).mockReturnValue({ value: [], onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), disabled: true, } as unknown as ReturnType); @@ -102,11 +126,60 @@ describe('MultiselectField', () => { expect(multiselect.disabled).toBe(true); }); - it('provides renderSelected callback', () => { + it('renders selected items using MultiselectfieldSelectedItem', () => { + render(); + + const selectedValues = screen.getAllByTestId('selected-value'); + expect(selectedValues).toHaveLength(1); + expect(screen.getByTestId('selected-item')).toBeInTheDocument(); + }); + + it('provides renderSelected callback that returns MultiselectfieldSelectedItem', () => { render(); const multiselect = vi.mocked(MultiSelect).mock.calls[0]![0]; - expect(multiselect.renderSelected).toBeDefined(); - expect(typeof multiselect.renderSelected).toBe('function'); + const mockParams: SelectedElementParams = { unselectButtonProps: { onClick: vi.fn() } as any }; + const mockOption: MultiSelectOption = { title: 'Test', value: 'test' }; + + const result = multiselect.renderSelected(mockParams, mockOption); + expect(result.type).toBe(MultiselectfieldSelectedItem); + expect(result.props).toEqual({ + option: mockOption, + params: mockParams, + }); + }); + + it('handles onBlur events', () => { + const mockOnBlur = vi.fn(); + vi.mocked(useField).mockReturnValue({ + value: ['opt1'], + onChange: vi.fn(), + onBlur: mockOnBlur, + onFocus: vi.fn(), + disabled: false, + } as unknown as ReturnType); + + render(); + const input = screen.getByTestId('multiselect-input'); + + fireEvent.blur(input); + expect(mockOnBlur).toHaveBeenCalled(); + }); + + it('handles onFocus events', () => { + const mockOnFocus = vi.fn(); + vi.mocked(useField).mockReturnValue({ + value: ['opt1'], + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: mockOnFocus, + disabled: false, + } as unknown as ReturnType); + + render(); + const input = screen.getByTestId('multiselect-input'); + + fireEvent.focus(input); + expect(mockOnFocus).toHaveBeenCalled(); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx index c760e0a18b..b25208b520 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx @@ -21,7 +21,7 @@ export const TextField: TDynamicFormField = const { stack } = useStack(); - const { value, onChange, onBlur, disabled } = useField(element, stack); + const { value, onChange, onBlur, onFocus, disabled } = useField(element, stack); const handleChange = useCallback( (event: React.ChangeEvent) => { @@ -38,6 +38,7 @@ export const TextField: TDynamicFormField = disabled, onChange: handleChange, onBlur, + onFocus, }; return ( diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx index c845b60898..56b79f48e4 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx @@ -51,6 +51,7 @@ describe('TextField', () => { value: '', onChange: vi.fn(), onBlur: vi.fn(), + onFocus: vi.fn(), disabled: false, touched: false, } as ReturnType; @@ -112,6 +113,16 @@ describe('TextField', () => { expect(mockFieldProps.onBlur).toHaveBeenCalled(); }); + it('should handle focus events', async () => { + const user = userEvent.setup(); + render(); + + const input = screen.getByTestId('test-id'); + await user.click(input); + + expect(mockFieldProps.onFocus).toHaveBeenCalled(); + }); + it('should respect disabled state', () => { vi.mocked(useField).mockReturnValue({ ...mockFieldProps, @@ -149,4 +160,21 @@ describe('TextField', () => { const input = screen.getByTestId('test-id'); expect(input).toHaveAttribute('type', 'text'); }); + + it('should handle focus and blur events for textarea', async () => { + const user = userEvent.setup(); + const textAreaElement = { + ...mockElement, + params: { ...mockElement.params, style: 'textarea' }, + } as unknown as IFormElement; + + render(); + + const textarea = screen.getByTestId('test-id'); + await user.click(textarea); + expect(mockFieldProps.onFocus).toHaveBeenCalled(); + + await user.tab(); + expect(mockFieldProps.onBlur).toHaveBeenCalled(); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts index c0c93c7c55..4d06f08b5a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts @@ -3,6 +3,9 @@ import { TDeepthLevelStack } from '@/components/organisms/Form/Validator'; import { useMemo } from 'react'; import { useDynamicForm } from '../../../context'; import { IFormElement } from '../../../types'; +import { useEvents } from '../../internal/useEvents'; +import { useMount } from '../../internal/useMount'; +import { useUnmount } from '../../internal/useUnmount'; import { useElementId } from '../useElementId'; export const useElement = ( @@ -10,6 +13,7 @@ export const useElement = ( stack: TDeepthLevelStack = [], ) => { const { values } = useDynamicForm(); + const { sendEvent } = useEvents(element); const hiddenRulesResult = useRuleEngine(values, { rules: element.hidden, runOnInitialize: true, @@ -22,6 +26,9 @@ export const useElement = ( return hiddenRulesResult.every(result => result.result === true); }, [hiddenRulesResult]); + useMount(() => sendEvent('onMount')); + useUnmount(() => sendEvent('onUnmount')); + return { id: useElementId(element, stack), originId: element.id, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts index 96a5cf1786..c0664d7400 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts @@ -2,13 +2,11 @@ import { renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { IDynamicFormContext, useDynamicForm } from '../../../context'; import { IFormElement } from '../../../types'; +import { useCallbacks } from '../../internal/useCallbacks'; import { useElement } from './useElement'; -vi.mock('../../../context', () => ({ - useDynamicForm: vi.fn().mockReturnValue({ - values: {}, - } as IDynamicFormContext), -})); +vi.mock('../../../context'); +vi.mock('../../../hooks/internal/useCallbacks'); describe('useElement', () => { beforeEach(() => { @@ -17,7 +15,11 @@ describe('useElement', () => { values: { test: 1, }, - } as IDynamicFormContext); + } as unknown as IDynamicFormContext); + + vi.mocked(useCallbacks).mockReturnValue({ + onEvent: vi.fn(), + }); vi.useFakeTimers(); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts index 5445253e24..a49f61746b 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts @@ -3,6 +3,7 @@ import { TDeepthLevelStack } from '@/components/organisms/Form/Validator'; import { useCallback, useMemo } from 'react'; import { useDynamicForm } from '../../../context'; import { IFormElement } from '../../../types'; +import { useEvents } from '../../internal/useEvents'; import { useElementId } from '../useElementId'; import { useValueDestination } from '../useValueDestination'; @@ -13,7 +14,7 @@ export const useField = ( const fieldId = useElementId(element, stack); const valueDestination = useValueDestination(element, stack); const { fieldHelpers, values } = useDynamicForm(); - + const { sendEvent, sendEventAsync } = useEvents(element); const { setValue, getValue, setTouched, getTouched } = fieldHelpers; const value = useMemo(() => getValue(valueDestination), [valueDestination, getValue]); @@ -36,14 +37,18 @@ export const useField = ( setValue(fieldId, valueDestination, value); setTouched(fieldId, true); - // TODO: Dispatch onChange event? + sendEventAsync('onChange'); }, - [fieldId, valueDestination, setValue, setTouched], + [fieldId, valueDestination, setValue, setTouched, sendEventAsync], ); const onBlur = useCallback(() => { - // TODO: Dispatch onBlur event? - }, []); + sendEvent('onBlur'); + }, [sendEvent]); + + const onFocus = useCallback(() => { + sendEvent('onFocus'); + }, [sendEvent]); return { value, @@ -51,5 +56,6 @@ export const useField = ( disabled: isDisabled, onChange, onBlur, + onFocus, }; }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts index 1544b00562..eafdf4d252 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts @@ -3,6 +3,7 @@ import { renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { IDynamicFormContext, useDynamicForm } from '../../../context'; import { IFormElement, TBaseFormElements } from '../../../types'; +import { useEvents } from '../../internal/useEvents'; import { useElementId } from '../useElementId'; import { useValueDestination } from '../useValueDestination'; import { useField } from './useField'; @@ -23,6 +24,10 @@ vi.mock('../useValueDestination', () => ({ useValueDestination: vi.fn(), })); +vi.mock('../../internal/useEvents', () => ({ + useEvents: vi.fn(), +})); + describe('useField', () => { const mockElement = { id: 'test-field', @@ -37,6 +42,8 @@ describe('useField', () => { const mockGetValue = vi.fn(); const mockSetTouched = vi.fn(); const mockGetTouched = vi.fn(); + const mockSendEvent = vi.fn(); + const mockSendEventAsync = vi.fn(); const mockFieldHelpers = { setValue: mockSetValue, @@ -51,6 +58,10 @@ describe('useField', () => { vi.mocked(useElementId).mockReturnValue('test-field-1-2'); vi.mocked(useValueDestination).mockReturnValue('test.path[1][2]'); vi.mocked(useRuleEngine).mockReturnValue([]); + vi.mocked(useEvents).mockReturnValue({ + sendEvent: mockSendEvent, + sendEventAsync: mockSendEventAsync, + } as unknown as ReturnType); vi.mocked(useDynamicForm).mockReturnValue({ fieldHelpers: mockFieldHelpers, values: {}, @@ -68,6 +79,7 @@ describe('useField', () => { disabled: false, onChange: expect.any(Function), onBlur: expect.any(Function), + onFocus: expect.any(Function), }); }); @@ -96,13 +108,34 @@ describe('useField', () => { }); describe('onChange', () => { - it('should update value and touched state', () => { + it('should update value, touched state and trigger async event', () => { const { result } = renderHook(() => useField(mockElement, mockStack)); result.current.onChange('new-value'); expect(mockSetValue).toHaveBeenCalledWith('test-field-1-2', 'test.path[1][2]', 'new-value'); expect(mockSetTouched).toHaveBeenCalledWith('test-field-1-2', true); + expect(mockSendEventAsync).toHaveBeenCalledWith('onChange'); + }); + }); + + describe('onBlur', () => { + it('should trigger blur event', () => { + const { result } = renderHook(() => useField(mockElement, mockStack)); + + result.current.onBlur(); + + expect(mockSendEvent).toHaveBeenCalledWith('onBlur'); + }); + }); + + describe('onFocus', () => { + it('should trigger focus event', () => { + const { result } = renderHook(() => useField(mockElement, mockStack)); + + result.current.onFocus(); + + expect(mockSendEvent).toHaveBeenCalledWith('onFocus'); }); }); @@ -145,6 +178,19 @@ describe('useField', () => { expect(result.current.disabled).toBe(false); }); + + it('should pass correct params to useRuleEngine', () => { + renderHook(() => useField(mockElement, mockStack)); + + expect(useRuleEngine).toHaveBeenCalledWith( + {}, + { + rules: mockElement.disable, + runOnInitialize: true, + executionDelay: 500, + }, + ); + }); }); it('should memoize value', () => { @@ -182,4 +228,13 @@ describe('useField', () => { expect(result.current.onBlur).toBe(initialOnBlur); }); + + it('should memoize onFocus', () => { + const { result, rerender } = renderHook(() => useField(mockElement, mockStack)); + const initialOnFocus = result.current.onFocus; + + rerender(); + + expect(result.current.onFocus).toBe(initialOnFocus); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/index.ts new file mode 100644 index 0000000000..50f3eded96 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/index.ts @@ -0,0 +1 @@ +export * from './useCallbacks'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallabacks.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallabacks.unit.test.ts new file mode 100644 index 0000000000..7de07ae2e5 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallabacks.unit.test.ts @@ -0,0 +1,34 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useDynamicForm } from '../../../context'; +import { useCallbacks } from './useCallbacks'; + +vi.mock('../../../context'); + +const mockUseDynamicForm = vi.mocked(useDynamicForm); + +describe('useCallbacks', () => { + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(useDynamicForm).mockReturnValue({ + callbacks: { + onEvent: vi.fn(), + }, + } as any); + }); + + it('should return callbacks from context', () => { + const mockCallbacks = { + onEvent: vi.fn(), + }; + + mockUseDynamicForm.mockReturnValue({ + callbacks: mockCallbacks, + } as any); + + const result = useCallbacks(); + + expect(result).toBe(mockCallbacks); + expect(mockUseDynamicForm).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallbacks.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallbacks.ts new file mode 100644 index 0000000000..917f2f4f27 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallbacks.ts @@ -0,0 +1,7 @@ +import { useDynamicForm } from '../../../context'; + +export const useCallbacks = () => { + const { callbacks } = useDynamicForm(); + + return callbacks; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/index.ts new file mode 100644 index 0000000000..dded7f0441 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/index.ts @@ -0,0 +1 @@ +export * from './useEvents'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types/index.ts new file mode 100644 index 0000000000..e50ba141ce --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types/index.ts @@ -0,0 +1,21 @@ +import { TBaseFormElements } from '../../../../types'; + +import { ICommonFieldParams } from '../../../../types'; + +import { IFormElement } from '../../../../types'; + +export type TElementEvent = + | 'onChange' + | 'onMount' + | 'onBlur' + | 'onFocus' + | 'onSubmit' + | 'onUnmount'; + +export interface IFormEventElement< + TElements extends string = TBaseFormElements, + TParams = ICommonFieldParams, +> extends IFormElement { + formattedValueDestination: string; + formattedId: string; +} diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.ts new file mode 100644 index 0000000000..ed87707b62 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.ts @@ -0,0 +1,45 @@ +import { formatId } from '@/components/organisms/Form/Validator/utils/format-id'; +import { formatValueDestination } from '@/components/organisms/Form/Validator/utils/format-value-destination'; +import debounce from 'lodash/debounce'; +import { useCallback } from 'react'; +import { useStack } from '../../../fields/FieldList/providers/StackProvider'; +import { IFormElement } from '../../../types'; +import { useCallbacks } from '../useCallbacks'; +import { IFormEventElement, TElementEvent } from './types'; + +export interface IUseEventParams { + asyncEventDelay?: number; +} + +export const useEvents = ( + element: IFormElement, + params: IUseEventParams = { asyncEventDelay: 500 }, +) => { + const { onEvent } = useCallbacks(); + const { stack } = useStack(); + const { asyncEventDelay } = params; + + const sendEvent = useCallback( + (eventName: TElementEvent) => { + const eventElement: IFormEventElement = { + ...element, + formattedValueDestination: formatValueDestination(element.valueDestination, stack || []), + formattedId: formatId(element.id, stack || []), + }; + + console.log(`Event ${eventName} triggered by ${eventElement.formattedId}`); + onEvent?.(eventName, eventElement); + }, + [onEvent, element, stack], + ); + + const sendEventAsync = useCallback( + debounce((eventName: TElementEvent) => sendEvent(eventName), asyncEventDelay), + [sendEvent, asyncEventDelay], + ); + + return { + sendEvent, + sendEventAsync, + }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.unit.test.ts new file mode 100644 index 0000000000..3808973be6 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.unit.test.ts @@ -0,0 +1,115 @@ +import { formatId } from '@/components/organisms/Form/Validator/utils/format-id'; +import { formatValueDestination } from '@/components/organisms/Form/Validator/utils/format-value-destination'; +import { renderHook } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useStack } from '../../../fields/FieldList/providers/StackProvider'; +import { useCallbacks } from '../useCallbacks'; +import { IFormEventElement } from './types'; +import { useEvents } from './useEvents'; + +vi.mock('@/components/organisms/Form/Validator/utils/format-id'); +vi.mock('@/components/organisms/Form/Validator/utils/format-value-destination'); +vi.mock('../../../fields/FieldList/providers/StackProvider'); +vi.mock('../useCallbacks'); + +const mockFormatId = vi.mocked(formatId); +const mockFormatValueDestination = vi.mocked(formatValueDestination); +const mockUseStack = vi.mocked(useStack); +const mockUseCallbacks = vi.mocked(useCallbacks); + +describe('useEvents', () => { + const mockElement = { + id: 'test-id', + valueDestination: 'test.destination', + element: 'textinput', + } as IFormEventElement; + + const mockOnEvent = vi.fn(); + const mockStack = [0, 0]; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseCallbacks.mockReturnValue({ onEvent: mockOnEvent }); + mockUseStack.mockReturnValue({ stack: mockStack }); + mockFormatId.mockReturnValue('formatted-id'); + mockFormatValueDestination.mockReturnValue('formatted.destination'); + mockUseCallbacks.mockReturnValue({ onEvent: mockOnEvent }); + vi.mocked(useCallbacks).mockReturnValue({ onEvent: mockOnEvent }); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it('should return sendEvent and sendEventAsync functions', () => { + const { result } = renderHook(() => useEvents(mockElement)); + expect(result.current.sendEvent).toBeInstanceOf(Function); + expect(result.current.sendEventAsync).toBeInstanceOf(Function); + }); + + it('should call onEvent with formatted element when sendEvent is called', () => { + const { result } = renderHook(() => useEvents(mockElement)); + + result.current.sendEvent('onChange'); + + expect(mockFormatId).toHaveBeenCalledWith('test-id', mockStack); + expect(mockFormatValueDestination).toHaveBeenCalledWith('test.destination', mockStack); + expect(mockOnEvent).toHaveBeenCalledWith('onChange', { + ...mockElement, + formattedId: 'formatted-id', + formattedValueDestination: 'formatted.destination', + }); + }); + + it('should handle undefined stack', () => { + mockUseStack.mockReturnValue({ stack: undefined }); + const { result } = renderHook(() => useEvents(mockElement)); + + result.current.sendEvent('onBlur'); + + expect(mockFormatId).toHaveBeenCalledWith('test-id', []); + expect(mockFormatValueDestination).toHaveBeenCalledWith('test.destination', []); + }); + + it('should handle undefined onEvent callback', () => { + mockUseCallbacks.mockReturnValue({ onEvent: undefined }); + const { result } = renderHook(() => useEvents(mockElement)); + + expect(() => result.current.sendEvent('onFocus')).not.toThrow(); + }); + + it('should use default asyncEventDelay when not provided', () => { + const { result } = renderHook(() => useEvents(mockElement)); + + vi.useFakeTimers(); + result.current.sendEventAsync('onChange'); + + vi.advanceTimersByTime(500); + expect(mockOnEvent).toHaveBeenCalled(); + vi.useRealTimers(); + }); + + it('should use provided asyncEventDelay', () => { + const { result } = renderHook(() => useEvents(mockElement, { asyncEventDelay: 1000 })); + + vi.useFakeTimers(); + result.current.sendEventAsync('onChange'); + + vi.advanceTimersByTime(1000); + expect(mockOnEvent).toHaveBeenCalled(); + vi.useRealTimers(); + }); + + it('should debounce async events', () => { + const { result } = renderHook(() => useEvents(mockElement)); + + vi.useFakeTimers(); + result.current.sendEventAsync('onChange'); + result.current.sendEventAsync('onChange'); + result.current.sendEventAsync('onChange'); + + vi.advanceTimersByTime(500); + expect(mockOnEvent).toHaveBeenCalledTimes(1); + vi.useRealTimers(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/index.ts new file mode 100644 index 0000000000..7a2256bb13 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/index.ts @@ -0,0 +1 @@ +export * from './useMount'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/useMount.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/useMount.ts new file mode 100644 index 0000000000..d6f3aa49dd --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/useMount.ts @@ -0,0 +1,13 @@ +import { useEffect, useRef } from 'react'; + +export const useMount = (callback: () => void) => { + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + callbackRef.current(); + }, [callbackRef]); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/useMount.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/useMount.unit.test.ts new file mode 100644 index 0000000000..f5bb553e8c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMount/useMount.unit.test.ts @@ -0,0 +1,35 @@ +import { renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { useMount } from './useMount'; + +describe('useMount', () => { + it('should call callback on mount', () => { + const mockCallback = vi.fn(); + renderHook(() => useMount(mockCallback)); + + expect(mockCallback).toHaveBeenCalledTimes(1); + }); + + it('should not call callback on rerender', () => { + const mockCallback = vi.fn(); + const { rerender } = renderHook(() => useMount(mockCallback)); + + rerender(); + + expect(mockCallback).toHaveBeenCalledTimes(1); + }); + + it('should use latest callback reference', () => { + const mockCallback1 = vi.fn(); + const mockCallback2 = vi.fn(); + + const { rerender } = renderHook(({ callback }) => useMount(callback), { + initialProps: { callback: mockCallback1 }, + }); + + rerender({ callback: mockCallback2 }); + + expect(mockCallback1).toHaveBeenCalledTimes(1); + expect(mockCallback2).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/index.ts new file mode 100644 index 0000000000..b4f8f8627f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/index.ts @@ -0,0 +1 @@ +export * from './useUnmount'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/useUnmount.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/useUnmount.ts new file mode 100644 index 0000000000..edafd80478 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/useUnmount.ts @@ -0,0 +1,13 @@ +import { useEffect, useRef } from 'react'; + +export const useUnmount = (callback: () => void) => { + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + return () => callbackRef.current(); + }, [callbackRef]); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/useUnmount.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/useUnmount.unit.test.ts new file mode 100644 index 0000000000..16f62945cc --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmount/useUnmount.unit.test.ts @@ -0,0 +1,40 @@ +import { renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { useUnmount } from './useUnmount'; + +describe('useUnmount', () => { + it('should call callback on unmount', () => { + const mockCallback = vi.fn(); + const { unmount } = renderHook(() => useUnmount(mockCallback)); + + expect(mockCallback).not.toHaveBeenCalled(); + + unmount(); + + expect(mockCallback).toHaveBeenCalledTimes(1); + }); + + it('should not call callback on rerender', () => { + const mockCallback = vi.fn(); + const { rerender } = renderHook(() => useUnmount(mockCallback)); + + rerender(); + + expect(mockCallback).not.toHaveBeenCalled(); + }); + + it('should use latest callback reference', () => { + const mockCallback1 = vi.fn(); + const mockCallback2 = vi.fn(); + + const { rerender, unmount } = renderHook(({ callback }) => useUnmount(callback), { + initialProps: { callback: mockCallback1 }, + }); + + rerender({ callback: mockCallback2 }); + unmount(); + + expect(mockCallback1).not.toHaveBeenCalled(); + expect(mockCallback2).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts index fd037ab4ef..bb98987d7e 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts @@ -1,6 +1,7 @@ import { FunctionComponent } from 'react'; import { IRule } from '../../hooks/useRuleEngine'; import { IValidationError, IValidationParams, TValidators } from '../../Validator'; +import { IFormEventElement, TElementEvent } from '../hooks/internal/useEvents/types'; export type TBaseFormElements = 'textinput' | 'fieldlist'; @@ -58,6 +59,7 @@ export interface IDynamicFormProps void; onFieldChange?: (fieldName: string, newValue: unknown, newValues: TValues) => void; onSubmit?: (values: TValues) => void; + onEvent?: (eventName: TElementEvent, element: IFormEventElement) => void; ref?: React.RefObject>; } diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.unit.test.ts index c7bdaffdcf..d094575b09 100644 --- a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useAsyncValidate.unit.test.ts @@ -1,23 +1,10 @@ import { renderHook } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { validate } from '../../../utils/validate'; import { useAsyncValidate } from './useAsyncValidate'; // Mock dependencies -vi.mock('../../../utils/validate', () => ({ - validate: vi.fn().mockReturnValue([ - { - id: 'name', - originId: 'name', - message: ['error'], - invalidValue: 'John', - }, - ]), -})); - -vi.mock('lodash/debounce', () => ({ - default: (fn: any) => fn, -})); +vi.mock('../../../utils/validate'); describe('useAsyncValidate', () => { const mockContext = { name: 'John' }; @@ -25,6 +12,21 @@ describe('useAsyncValidate', () => { beforeEach(() => { vi.clearAllMocks(); + + vi.mocked(validate).mockReturnValue([ + { + id: 'name', + originId: 'name', + message: ['error'], + invalidValue: 'John', + }, + ]); + + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); }); it('should initialize with empty validation errors', () => { @@ -50,6 +52,8 @@ describe('useAsyncValidate', () => { }), ); + vi.advanceTimersByTime(500); + expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: false }); expect(result.current).toEqual([ { @@ -70,6 +74,8 @@ describe('useAsyncValidate', () => { }), ); + vi.advanceTimersByTime(500); + expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: true }); }); @@ -88,6 +94,8 @@ describe('useAsyncValidate', () => { const newContext = { name: 'Jane' }; rerender({ context: newContext }); + vi.advanceTimersByTime(500); + expect(validate).toHaveBeenCalledWith(newContext, mockSchema, { abortEarly: false }); }); @@ -106,6 +114,8 @@ describe('useAsyncValidate', () => { const newSchema = [{ id: 'email', validators: [], rules: [] }]; rerender({ schema: newSchema }); + vi.advanceTimersByTime(500); + expect(validate).toHaveBeenCalledWith(mockContext, newSchema, { abortEarly: false }); }); }); diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.unit.test.ts index c48703a204..f31a68aae1 100644 --- a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useSyncValidate.unit.test.ts @@ -4,16 +4,7 @@ import { validate } from '../../../utils/validate'; import { useSyncValidate } from './useSyncValidate'; // Mock dependencies -vi.mock('../../../utils/validate', () => ({ - validate: vi.fn().mockReturnValue([ - { - id: 'name', - originId: 'name', - message: ['error'], - invalidValue: 'John', - }, - ]), -})); +vi.mock('../../../utils/validate'); describe('useSyncValidate', () => { const mockContext = { name: 'John' }; @@ -21,6 +12,15 @@ describe('useSyncValidate', () => { beforeEach(() => { vi.clearAllMocks(); + + vi.mocked(validate).mockReturnValue([ + { + id: 'name', + originId: 'name', + message: ['error'], + invalidValue: 'John', + }, + ]); }); it('should initialize with empty validation errors when validateSync is false', () => { diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.unit.test.ts index 7dc39283ba..91e67951b3 100644 --- a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidatorRef/useValidatorRef.unit.test.ts @@ -1,5 +1,6 @@ import { renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { IValidatorContext } from '../../../context'; import { useValidator } from '../../external/useValidator/useValidator'; import { useValidatorRef } from './useValidatorRef'; @@ -16,6 +17,10 @@ describe('useValidatorRef', () => { beforeEach(() => { vi.clearAllMocks(); + + vi.mocked(useValidator).mockReturnValue({ + validate: mockValidate, + } as unknown as IValidatorContext); }); it('should return context from useValidator', () => { diff --git a/packages/ui/src/setupTests.ts b/packages/ui/src/setupTests.ts index b2596a8a6d..49d9cbbfd5 100644 --- a/packages/ui/src/setupTests.ts +++ b/packages/ui/src/setupTests.ts @@ -1,7 +1,7 @@ import '@testing-library/jest-dom'; import matchers from '@testing-library/jest-dom/matchers'; import { cleanup } from '@testing-library/react'; -import { afterEach, expect } from 'vitest'; +import { afterEach, expect, vi } from 'vitest'; if (matchers) { // Extend Vitest's expect with jest-dom matchers @@ -11,4 +11,7 @@ if (matchers) { // runs a cleanup after each test case (e.g. clearing jsdom) afterEach(() => { cleanup(); + vi.clearAllMocks(); + vi.resetAllMocks(); + vi.restoreAllMocks(); }); From babea18d7075d5ba262060c1e5f2a3d5a5dd635e Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Wed, 18 Dec 2024 12:22:40 +0200 Subject: [PATCH 18/54] feat: implemented fields extend & removed elementsMap --- .../Form/DynamicForm/DynamicForm.tsx | 9 ++-- .../DynamicForm/DynamicForm.unit.test.tsx | 12 ++--- .../hooks/internal/useTouched/useTouched.ts | 2 +- .../useValidationSchema.ts | 2 +- .../repositories/fields-repository.ts | 44 +++++++++++++++++++ .../Form/DynamicForm/repositories/index.ts | 1 + .../organisms/Form/DynamicForm/types/index.ts | 22 ++++------ 7 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/repositories/index.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx index 84b616d94a..8335f563b8 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -8,13 +8,14 @@ import { useFieldHelpers } from './hooks/internal/useFieldHelpers'; import { useTouched } from './hooks/internal/useTouched'; import { useValidationSchema } from './hooks/internal/useValidationSchema'; import { useValues } from './hooks/internal/useValues'; +import { extendFieldsRepository, getFieldsRepository } from './repositories'; import { IDynamicFormProps } from './types'; export const DynamicFormV2: FunctionComponent = ({ elements, values: initialValues, validationParams, - elementsMap, + fieldExtends, onChange, onFieldChange, onSubmit, @@ -36,18 +37,18 @@ export const DynamicFormV2: FunctionComponent = ({ values: valuesApi.values, submit, fieldHelpers, - elementsMap, + elementsMap: fieldExtends ? extendFieldsRepository(fieldExtends) : getFieldsRepository(), callbacks: { onEvent, }, }), - [touchedApi.touched, valuesApi.values, submit, fieldHelpers, elementsMap, onEvent], + [touchedApi.touched, valuesApi.values, submit, fieldHelpers, fieldExtends, onEvent], ); return ( - + ); 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 4d984ff72c..cd9b80d148 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 @@ -9,7 +9,7 @@ import { useFieldHelpers } from './hooks/internal/useFieldHelpers'; import { useTouched } from './hooks/internal/useTouched'; import { useValidationSchema } from './hooks/internal/useValidationSchema'; import { useValues } from './hooks/internal/useValues'; -import { ICommonFieldParams, IDynamicFormProps, IFormElement, TBaseFormElements } from './types'; +import { ICommonFieldParams, IDynamicFormProps, IFormElement } from './types'; // Mock dependencies vi.mock('../../Renderer'); @@ -75,20 +75,19 @@ describe('DynamicFormV2', () => { elements: [], values: {}, validationParams: {}, - elementsMap: {}, onChange: vi.fn(), onFieldChange: vi.fn(), onSubmit: vi.fn(), onEvent: vi.fn(), - } as unknown as IDynamicFormProps; + } as unknown as IDynamicFormProps; it('should render without crashing', () => { render(); }); it('should pass elements to useValidationSchema', () => { - const elements = [{ id: 'test', element: 'text' }] as unknown as Array< - IFormElement + const elements = [{ id: 'test', element: 'textfield' }] as unknown as Array< + IFormElement >; render(); expect(useValidationSchema).toHaveBeenCalledWith(elements); @@ -127,6 +126,7 @@ describe('DynamicFormV2', () => { onSubmit: mockProps.onSubmit, }); }); + it('should pass context to DynamicFormContext.Provider', () => { const touchedMock = { touched: { field1: true }, @@ -162,7 +162,7 @@ describe('DynamicFormV2', () => { values: valuesMock.values, submit: submitMock.submit, fieldHelpers: fieldHelpersMock, - elementsMap: mockProps.elementsMap, + elementsMap: mockProps.fieldExtends ? expect.any(Object) : expect.any(Object), callbacks: { onEvent: mockProps.onEvent, }, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts index 5cde67b769..71401e9d6a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts @@ -3,7 +3,7 @@ import { IFormElement } from '../../../types'; import { generateTouchedMapForAllElements } from './helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements'; import { ITouchedState } from './types'; -export const useTouched = (elements: IFormElement[], context: object) => { +export const useTouched = (elements: Array>, context: object) => { const [touched, setTouchedState] = useState({}); const setFieldTouched = useCallback((fieldName: string, isTouched: boolean) => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts index 2d71dd4196..07ddd06181 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import { convertFormElementsToValidationSchema } from '../../../helpers/convert-form-emenents-to-validation-schema'; import { IFormElement } from '../../../types'; -export const useValidationSchema = (elements: IFormElement[]) => { +export const useValidationSchema = (elements: Array>) => { const validationSchema = useMemo(() => { return convertFormElementsToValidationSchema(elements); }, [elements]); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts new file mode 100644 index 0000000000..d340942635 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts @@ -0,0 +1,44 @@ +import { AutocompleteField } from '../fields/AutocompleteField'; +import { CheckboxField } from '../fields/CheckboxField'; +import { CheckboxListField } from '../fields/CheckboxList'; +import { DateField } from '../fields/DateField'; +import { FieldList } from '../fields/FieldList'; +import { MultiselectField } from '../fields/MultiselectField'; +import { TextField } from '../fields/TextField'; +import { TDynamicFormField } from '../types'; + +export const baseFields = { + autocompletefield: AutocompleteField, + checkboxfield: CheckboxField, + checkboxlistfield: CheckboxListField, + datefield: DateField, + multiselectfield: MultiselectField, + textfield: TextField, + fieldlist: FieldList, +} as const; + +export type TBaseFields = keyof typeof baseFields & string; + +export let fieldsRepository = { + ...baseFields, +}; + +export const getField = (fieldType: T) => { + return fieldsRepository[fieldType]; +}; + +export const extendFieldsRepository = ( + fields: Record>, +) => { + const updatedRepository = { ...fieldsRepository, ...fields }; + fieldsRepository = updatedRepository; + + return updatedRepository; +}; + +export const getFieldsRepository = < + TElements extends string = TBaseFields, + TParams = unknown, +>() => { + return fieldsRepository as Record>; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/index.ts new file mode 100644 index 0000000000..1029e92cdb --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/index.ts @@ -0,0 +1 @@ +export * from './fields-repository'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts index bb98987d7e..51b5779c19 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts @@ -3,14 +3,12 @@ import { IRule } from '../../hooks/useRuleEngine'; import { IValidationError, IValidationParams, TValidators } from '../../Validator'; import { IFormEventElement, TElementEvent } from '../hooks/internal/useEvents/types'; -export type TBaseFormElements = 'textinput' | 'fieldlist'; - export interface ICommonFieldParams { label?: string; placeholder?: string; } -export interface IFormElement { +export interface IFormElement { id: string; valueDestination: string; element: TElements; @@ -31,35 +29,33 @@ export interface IFormRef { } export type TDynamicFormElement< - TElements extends string = TBaseFormElements, + TElements extends string = string, TParams = unknown, > = FunctionComponent<{ element: IFormElement; }>; export type TDynamicFormField< - TElements extends string = TBaseFormElements, + TElements extends string = string, TParams = ICommonFieldParams, > = FunctionComponent<{ element: IFormElement; children?: React.ReactNode | React.ReactNode[]; }>; -export type TElementsMap = Record< - TElements, - TDynamicFormElement ->; +export type TElementsMap = Record>; -export interface IDynamicFormProps { +export interface IDynamicFormProps { values: TValues; - elements: Array>; - elementsMap: TElementsMap; + elements: Array>; + + fieldExtends?: Record>; validationParams?: IValidationParams; onChange?: (newValues: TValues) => void; onFieldChange?: (fieldName: string, newValue: unknown, newValues: TValues) => void; onSubmit?: (values: TValues) => void; - onEvent?: (eventName: TElementEvent, element: IFormEventElement) => void; + onEvent?: (eventName: TElementEvent, element: IFormEventElement) => void; ref?: React.RefObject>; } From 7f07d19b3ab524bb2bf5302351c5f74b59172c1d Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Wed, 18 Dec 2024 13:12:09 +0200 Subject: [PATCH 19/54] feat: added select field & fixed types & tests --- .../inputs/DropdownInput/DropdownInput.tsx | 2 + .../molecules/inputs/DropdownInput/types.ts | 1 + .../controls/SubmitButton/SubmitButton.tsx | 6 +- .../SubmitButton/SubmitButton.unit.test.tsx | 6 +- .../AutocompleteField/AutocompleteField.tsx | 6 +- .../AutocompleteField.unit.test.tsx | 6 +- .../CheckboxField/CheckboxField.unit.test.tsx | 4 +- .../fields/CheckboxList/CheckboxList.tsx | 6 +- .../CheckboxList/CheckboxList.unit.test.tsx | 6 +- .../fields/DateField/DateField.tsx | 4 +- .../fields/DateField/DateField.unit.test.tsx | 6 +- .../fields/FieldList/FieldList.tsx | 11 +- .../hooks/useFieldList/useFieldList.ts | 4 +- .../useFieldList/useFieldList.unit.test.ts | 4 +- .../MultiselectField/MultiselectField.tsx | 6 +- .../MultiselectField.unit.test.tsx | 6 +- .../fields/SelectField/SelectField.tsx | 43 ++++ .../SelectField/SelectField.unit.test.tsx | 239 ++++++++++++++++++ .../DynamicForm/fields/SelectField/index.ts | 1 + .../fields/TextField/TextField.tsx | 4 +- .../fields/TextField/TextField.unit.test.tsx | 12 +- .../useElement/useElement.unit.test.ts | 2 +- .../external/useField/useField.unit.test.ts | 7 +- .../hooks/internal/useEvents/types/index.ts | 8 +- .../layouts/FieldLayout/FieldLayout.tsx | 2 +- .../repositories/fields-repository.ts | 6 +- .../organisms/Form/DynamicForm/types/index.ts | 7 +- .../useValidate/useValidate.unit.test.ts | 2 +- 28 files changed, 344 insertions(+), 73 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/index.ts diff --git a/packages/ui/src/components/molecules/inputs/DropdownInput/DropdownInput.tsx b/packages/ui/src/components/molecules/inputs/DropdownInput/DropdownInput.tsx index 3c7e313556..bfc15d8cae 100644 --- a/packages/ui/src/components/molecules/inputs/DropdownInput/DropdownInput.tsx +++ b/packages/ui/src/components/molecules/inputs/DropdownInput/DropdownInput.tsx @@ -53,6 +53,7 @@ export const DropdownInput: FunctionComponent = ({ testId, onChange, onBlur, + onFocus, props, textInputClassName, }) => { @@ -119,6 +120,7 @@ export const DropdownInput: FunctionComponent = ({ {searchable ? ( diff --git a/packages/ui/src/components/molecules/inputs/DropdownInput/types.ts b/packages/ui/src/components/molecules/inputs/DropdownInput/types.ts index caa8ac76d0..24536155c2 100644 --- a/packages/ui/src/components/molecules/inputs/DropdownInput/types.ts +++ b/packages/ui/src/components/molecules/inputs/DropdownInput/types.ts @@ -22,6 +22,7 @@ export interface DropdownInputProps { openOnFocus?: boolean; onChange: (value: string, inputName: string) => void; onBlur?: (event: FocusEvent) => void; + onFocus?: (event: FocusEvent) => void; props?: { trigger?: Pick, 'className'> & { icon?: React.ReactNode; 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 b977e71623..ba1f050433 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 @@ -4,16 +4,14 @@ import { useValidator } from '../../../Validator'; import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external/useElement'; import { useField } from '../../hooks/external/useField'; -import { TBaseFormElements, TDynamicFormElement } from '../../types'; +import { TDynamicFormElement } from '../../types'; export interface ISubmitButtonParams { disableWhenFormIsInvalid?: boolean; text?: string; } -export const SubmitButton: TDynamicFormElement = ({ - element, -}) => { +export const SubmitButton: TDynamicFormElement = ({ element }) => { const { id } = useElement(element); const { disabled: _disabled } = useField(element); const { submit } = useDynamicForm(); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx index ad1132a64e..75bdda788d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx @@ -7,7 +7,7 @@ import { useValidator } from '../../../Validator'; import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external'; import { useField } from '../../hooks/external/useField'; -import { IFormElement, TBaseFormElements } from '../../types'; +import { IFormElement } from '../../types'; import { ISubmitButtonParams, SubmitButton } from './SubmitButton'; vi.mock('@/components/atoms', () => ({ @@ -31,8 +31,8 @@ describe('SubmitButton', () => { id: 'test-button', params: {}, valueDestination: 'test.path', - element: '' as TBaseFormElements, - } as IFormElement; + element: '', + } as unknown as IFormElement; const mockSubmit = vi.fn(); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx index 5a8fe5b570..9db014bbf9 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx @@ -2,7 +2,7 @@ import { AutocompleteInput } from '@/components/molecules'; import { createTestId } from '@/components/organisms/Renderer'; import { useField } from '../../hooks/external/useField'; import { FieldLayout } from '../../layouts/FieldLayout'; -import { TBaseFormElements, TDynamicFormField } from '../../types'; +import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; export interface IAutocompleteFieldOption { @@ -15,9 +15,7 @@ export interface IAutocompleteFieldParams { options: IAutocompleteFieldOption[]; } -export const AutocompleteField: TDynamicFormField = ({ - element, -}) => { +export const AutocompleteField: TDynamicFormField = ({ element }) => { const { params } = element; const { stack } = useStack(); const { value, onChange, onBlur, onFocus, disabled } = useField( diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx index 2a846cf60b..1f51a88f41 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx @@ -3,7 +3,7 @@ import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external/useField'; -import { IFormElement, TBaseFormElements } from '../../types'; +import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { AutocompleteField, IAutocompleteFieldParams } from './AutocompleteField'; @@ -52,7 +52,7 @@ describe('AutocompleteField', () => { placeholder: 'Select an option', options: mockOptions, }, - } as IFormElement; + } as unknown as IFormElement; const mockFieldProps = { value: '', @@ -131,7 +131,7 @@ describe('AutocompleteField', () => { it('should use default params when none provided', () => { const elementWithoutParams = { id: 'test-autocomplete', - } as unknown as IFormElement; + } as unknown as IFormElement; render(); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx index 1c13ac8735..a945964839 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx @@ -2,7 +2,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; -import { IFormElement, TBaseFormElements } from '../../types'; +import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { CheckboxField } from './CheckboxField'; @@ -34,7 +34,7 @@ describe('CheckboxField', () => { const mockElement = { id: 'test-checkbox', type: '', - } as unknown as IFormElement; + } as unknown as IFormElement; const mockFieldProps = { value: false, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx index b8c80d7213..7ee0402160 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx @@ -3,7 +3,7 @@ import { Checkbox } from '@/components/atoms'; import { createTestId } from '@/components/organisms/Renderer'; import { useField } from '../../hooks/external'; import { FieldLayout } from '../../layouts/FieldLayout'; -import { TBaseFormElements, TDynamicFormField } from '../../types'; +import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; export interface ICheckboxListOption { @@ -15,9 +15,7 @@ export interface ICheckboxListFieldParams { options: ICheckboxListOption[]; } -export const CheckboxListField: TDynamicFormField = ({ - element, -}) => { +export const CheckboxListField: TDynamicFormField = ({ element }) => { const { options = [] } = element.params || {}; const { stack } = useStack(); const { value, onChange, onFocus, onBlur, disabled } = useField(element, stack); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx index 4806afdc82..8887f97e34 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx @@ -2,7 +2,7 @@ import { createTestId } from '@/components/organisms/Renderer'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external/useField'; -import { IFormElement, TBaseFormElements } from '../../types'; +import { IFormElement } from '../../types'; import { CheckboxListField, ICheckboxListFieldParams } from './CheckboxList'; // Mock dependencies @@ -50,7 +50,7 @@ describe('CheckboxListField', () => { params: { options: mockOptions, }, - } as unknown as IFormElement; + } as unknown as IFormElement; beforeEach(() => { cleanup(); @@ -178,7 +178,7 @@ describe('CheckboxListField', () => { const emptyElement = { ...mockElement, params: { options: [] }, - } as unknown as IFormElement; + } as unknown as IFormElement; render(); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx index ca7b615979..3b36f7102b 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx @@ -8,7 +8,7 @@ import { createTestId } from '@/components/organisms/Renderer'; import { useCallback } from 'react'; import { useField } from '../../hooks/external/useField'; import { FieldLayout } from '../../layouts/FieldLayout'; -import { TBaseFormElements, TDynamicFormField } from '../../types'; +import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; export interface IDateFieldParams { @@ -17,7 +17,7 @@ export interface IDateFieldParams { outputFormat?: 'date' | 'iso'; } -export const DateField: TDynamicFormField = ({ element }) => { +export const DateField: TDynamicFormField = ({ element }) => { const { disableFuture = false, disablePast = false, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx index e4a373e7b0..5d1a7c2e3d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx @@ -3,7 +3,7 @@ import { DatePickerInput } from '@/components/molecules'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external/useField'; -import { IFormElement, TBaseFormElements } from '../../types'; +import { IFormElement } from '../../types'; import { DateField, IDateFieldParams } from './DateField'; // Mock dependencies @@ -67,7 +67,7 @@ describe('DateField', () => { disablePast: false, outputFormat: 'iso', }, - } as unknown as IFormElement; + } as unknown as IFormElement; it('renders DatePickerInput with correct props', () => { render(); @@ -137,7 +137,7 @@ describe('DateField', () => { }); it('passes correct params to DatePickerInput', () => { - const elementWithParams: IFormElement = { + const elementWithParams: IFormElement = { ...mockElement, params: { disableFuture: true, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx index 8ed1d43ac1..99369c0204 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx @@ -3,22 +3,19 @@ import { Button } from '@/components/atoms'; import { Renderer, TRendererSchema } from '@/components/organisms/Renderer'; import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external'; -import { TBaseFormElements, TDynamicFormElement } from '../../types'; -import { IUseFieldParams, useFieldList } from './hooks/useFieldList'; +import { TDynamicFormField } from '../../types'; +import { useFieldList } from './hooks/useFieldList'; import { StackProvider, useStack } from './providers/StackProvider'; export type TFieldListValueType = T[]; -export interface IFieldListOptions { +export interface IFieldListParams { defaultValue: AnyObject; addButtonLabel?: string; removeButtonLabel?: string; } -export const FieldList: TDynamicFormElement< - TBaseFormElements, - { addButtonLabel: string; removeButtonLabel: string } & IUseFieldParams -> = props => { +export const FieldList: TDynamicFormField = props => { const { elementsMap } = useDynamicForm(); const { stack } = useStack(); const { element } = props; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts index 6400fec2b8..43e99445c2 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.ts @@ -1,13 +1,13 @@ import { useCallback } from 'react'; import { useField } from '../../../../hooks/external'; -import { IFormElement, TBaseFormElements } from '../../../../types'; +import { IFormElement } from '../../../../types'; import { useStack } from '../../providers/StackProvider'; export interface IUseFieldParams { defaultValue: T; } export interface IUseFieldListProps { - element: IFormElement>; + element: IFormElement>; } export const useFieldList = ({ element }: IUseFieldListProps) => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.unit.test.ts index e0d2469c29..56b2182a91 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/hooks/useFieldList/useFieldList.unit.test.ts @@ -1,7 +1,7 @@ import { renderHook } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../../../hooks/external'; -import { IFormElement, TBaseFormElements } from '../../../../types'; +import { IFormElement } from '../../../../types'; import { useStack } from '../../providers/StackProvider'; import { IUseFieldParams, useFieldList } from './useFieldList'; @@ -16,7 +16,7 @@ describe('useFieldList', () => { params: { defaultValue: { test: 'value' }, }, - } as unknown as IFormElement>; + } as unknown as IFormElement>; const mockOnChange = vi.fn(); const mockStack = [0]; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx index fe0583b951..e0a054bfde 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx @@ -2,7 +2,7 @@ import { MultiSelect, MultiSelectOption, MultiSelectValue } from '@/components/m import { SelectedElementParams } from '@/components/molecules/inputs/MultiSelect/types'; import { useCallback } from 'react'; import { useField } from '../../hooks/external'; -import { TBaseFormElements, TDynamicFormField } from '../../types'; +import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { MultiselectfieldSelectedItem } from './MultiselectFieldSelectedItem'; @@ -10,9 +10,7 @@ export interface IMultiselectFieldParams { options: MultiSelectOption[]; } -export const MultiselectField: TDynamicFormField = ({ - element, -}) => { +export const MultiselectField: TDynamicFormField = ({ element }) => { const { stack } = useStack(); const { value, onChange, onBlur, onFocus, disabled } = useField( element, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx index 61b6c5e630..328bde8195 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx @@ -3,7 +3,7 @@ import { SelectedElementParams } from '@/components/molecules/inputs/MultiSelect import { fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; -import { IFormElement, TBaseFormElements } from '../../types'; +import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { IMultiselectFieldParams, MultiselectField } from './MultiselectField'; import { MultiselectfieldSelectedItem } from './MultiselectFieldSelectedItem'; @@ -52,7 +52,7 @@ describe('MultiselectField', () => { params: { options: mockOptions, }, - } as unknown as IFormElement; + } as unknown as IFormElement; beforeEach(() => { vi.clearAllMocks(); @@ -103,7 +103,7 @@ describe('MultiselectField', () => { const elementWithoutOptions = { ...mockElement, params: {}, - } as unknown as IFormElement; + } as unknown as IFormElement; render(); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx new file mode 100644 index 0000000000..6ad40604b6 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx @@ -0,0 +1,43 @@ +import { DropdownInput } from '@/components/molecules'; +import { createTestId } from '@/components/organisms/Renderer'; +import { useElement, useField } from '../../hooks/external'; +import { IFieldLayoutBaseParams } from '../../layouts/FieldLayout'; +import { TDynamicFormField } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; + +export interface ISelectOption { + value: string; + label: string; +} + +export interface ISelectFieldParams extends IFieldLayoutBaseParams { + placeholder?: string; + options: ISelectOption[]; +} + +export const SelectField: TDynamicFormField = ({ element }) => { + const { stack } = useStack(); + const { id } = useElement(element, stack); + const { value, disabled, onChange, onBlur, onFocus } = useField( + element, + stack, + ); + + const { placeholder, options = [] } = element.params || {}; + + return ( + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx new file mode 100644 index 0000000000..92af5df9ae --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx @@ -0,0 +1,239 @@ +import { DropdownInput } from '@/components/molecules'; +import { createTestId } from '@/components/organisms/Renderer'; +import { fireEvent, render } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useElement, useField } from '../../hooks/external'; +import { TBaseFields } from '../../repositories/fields-repository'; +import { IFormElement } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; +import { ISelectFieldParams, SelectField } from './SelectField'; + +// Mock dependencies +vi.mock('@/components/molecules', () => ({ + DropdownInput: vi.fn(({ options, onChange, onFocus, onBlur, value }: any) => ( + + )), +})); + +vi.mock('@/components/organisms/Renderer', () => ({ + createTestId: vi.fn(), +})); + +vi.mock('../../hooks/external', () => ({ + useElement: vi.fn(), + useField: vi.fn(), +})); + +vi.mock('../FieldList/providers/StackProvider', () => ({ + useStack: vi.fn(), +})); + +describe('SelectField', () => { + const mockElement = { + id: 'test-id', + params: { + placeholder: 'Select an option', + options: [ + { value: '1', label: 'Option 1' }, + { value: '2', label: 'Option 2' }, + ], + }, + } as IFormElement; + + const mockStack = [0]; + const mockTestId = 'test-select-field'; + + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(useStack).mockReturnValue({ stack: mockStack }); + vi.mocked(useElement).mockReturnValue({ id: mockElement.id } as ReturnType); + vi.mocked(useField).mockReturnValue({ + value: undefined, + disabled: false, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }); + vi.mocked(createTestId).mockReturnValue(mockTestId); + }); + + it('should render DropdownInput with correct props', () => { + render(); + + expect(DropdownInput).toHaveBeenCalledWith( + expect.objectContaining({ + name: mockElement.id, + options: mockElement.params?.options || [], + testId: mockTestId, + placeholdersParams: { + placeholder: mockElement.params?.placeholder || '', + }, + disabled: false, + }), + expect.any(Object), + ); + }); + + it('should handle empty params gracefully', () => { + const elementWithoutParams = { + id: 'test-id', + } as IFormElement; + + render(); + + expect(DropdownInput).toHaveBeenCalledWith( + expect.objectContaining({ + options: [], + placeholdersParams: { + placeholder: '', + }, + }), + expect.any(Object), + ); + }); + + it('should pass through field handlers from useField', () => { + const mockHandlers = { + value: '1', + disabled: true, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }; + + vi.mocked(useField).mockReturnValue(mockHandlers); + + render(); + + expect(DropdownInput).toHaveBeenCalledWith( + expect.objectContaining({ + value: mockHandlers.value, + disabled: mockHandlers.disabled, + onChange: mockHandlers.onChange, + onBlur: mockHandlers.onBlur, + onFocus: mockHandlers.onFocus, + }), + expect.any(Object), + ); + }); + + it('should trigger onBlur when dropdown is closed', async () => { + const mockHandlers = { + value: '1', + disabled: false, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }; + + vi.mocked(useField).mockReturnValue(mockHandlers); + + const { getByRole } = render(); + + const trigger = getByRole('combobox'); + fireEvent.blur(trigger); + + expect(mockHandlers.onBlur).toHaveBeenCalled(); + }); + + it('should trigger onFocus when dropdown input is focused', async () => { + const mockHandlers = { + value: '1', + disabled: false, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }; + + vi.mocked(useField).mockReturnValue(mockHandlers); + + const { getByRole } = render(); + + const trigger = getByRole('combobox'); + await trigger.focus(); + + expect(mockHandlers.onFocus).toHaveBeenCalled(); + }); + + it('should render options when dropdown is opened', async () => { + const mockHandlers = { + value: undefined, + disabled: false, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }; + + vi.mocked(useField).mockReturnValue(mockHandlers); + + const { getByRole, getByText } = render(); + + const trigger = getByRole('combobox'); + fireEvent.click(trigger); + + // Check that both options from mockElement are rendered + expect(getByText('Option 1')).toBeInTheDocument(); + expect(getByText('Option 2')).toBeInTheDocument(); + }); + + it('should call on change callback on value change', async () => { + const mockHandlers = { + value: undefined, + disabled: false, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }; + + vi.mocked(useField).mockReturnValue(mockHandlers); + + const { getByRole } = render(); + + const trigger = getByRole('combobox'); + fireEvent.change(trigger, { target: { value: '1' } }); + + expect(mockHandlers.onChange).toHaveBeenCalledWith('1'); + }); + + it('should show selected option in trigger button', async () => { + const mockHandlers = { + value: '2', + disabled: false, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }; + + vi.mocked(useField).mockReturnValue(mockHandlers); + + const { getByRole } = render(); + + const trigger = getByRole('combobox'); + expect(trigger).toHaveTextContent('Option 2'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/index.ts new file mode 100644 index 0000000000..ec0bc2da93 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/index.ts @@ -0,0 +1 @@ +export * from './SelectField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx index b25208b520..578185e866 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx @@ -5,7 +5,7 @@ import { useCallback } from 'react'; import { useField } from '../../hooks/external'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; -import { TBaseFormElements, TDynamicFormField } from '../../types'; +import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { serializeTextFieldValue } from './helpers'; @@ -15,7 +15,7 @@ export interface ITextFieldParams { placeholder?: string; } -export const TextField: TDynamicFormField = ({ element }) => { +export const TextField: TDynamicFormField = ({ element }) => { const { params } = element; const { valueType = 'string', style = 'text', placeholder } = params || {}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx index 56b79f48e4..d1592b6cd0 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx @@ -3,7 +3,7 @@ import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; -import { IFormElement, TBaseFormElements } from '../../types'; +import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { ITextFieldParams, TextField } from './TextField'; @@ -45,7 +45,7 @@ describe('TextField', () => { style: 'text', placeholder: 'Enter text', }, - } as IFormElement; + } as unknown as IFormElement; const mockFieldProps = { value: '', @@ -74,7 +74,7 @@ describe('TextField', () => { const textAreaElement = { ...mockElement, params: { ...mockElement.params, style: 'textarea' }, - } as unknown as IFormElement; + } as unknown as IFormElement; render(); @@ -85,7 +85,7 @@ describe('TextField', () => { const numberElement = { ...mockElement, params: { ...mockElement.params, valueType: 'number' }, - } as unknown as IFormElement; + } as unknown as IFormElement; render(); @@ -153,7 +153,7 @@ describe('TextField', () => { it('should use default params when none provided', () => { const elementWithoutParams = { id: 'test-field', - } as unknown as IFormElement; + } as unknown as IFormElement; render(); @@ -166,7 +166,7 @@ describe('TextField', () => { const textAreaElement = { ...mockElement, params: { ...mockElement.params, style: 'textarea' }, - } as unknown as IFormElement; + } as unknown as IFormElement; render(); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts index c0664d7400..981605fa36 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts @@ -97,7 +97,7 @@ describe('useElement', () => { rerender(); - await vi.advanceTimersByTimeAsync(500); + await vi.advanceTimersByTimeAsync(550); expect(result.current.hidden).toBe(true); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts index eafdf4d252..b60d864bc1 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts @@ -2,8 +2,9 @@ import { IRuleExecutionResult, useRuleEngine } from '@/components/organisms/Form import { renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { IDynamicFormContext, useDynamicForm } from '../../../context'; -import { IFormElement, TBaseFormElements } from '../../../types'; +import { ICommonFieldParams, IFormElement } from '../../../types'; import { useEvents } from '../../internal/useEvents'; +import { IFormEventElement } from '../../internal/useEvents/types'; import { useElementId } from '../useElementId'; import { useValueDestination } from '../useValueDestination'; import { useField } from './useField'; @@ -33,8 +34,8 @@ describe('useField', () => { id: 'test-field', valueDestination: 'test.path', disable: [], - element: {} as TBaseFormElements, - } as IFormElement; + element: {} as IFormEventElement, + } as unknown as IFormElement; const mockStack = [1, 2]; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types/index.ts index e50ba141ce..f76d4e9530 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types/index.ts @@ -1,5 +1,3 @@ -import { TBaseFormElements } from '../../../../types'; - import { ICommonFieldParams } from '../../../../types'; import { IFormElement } from '../../../../types'; @@ -12,10 +10,8 @@ export type TElementEvent = | 'onSubmit' | 'onUnmount'; -export interface IFormEventElement< - TElements extends string = TBaseFormElements, - TParams = ICommonFieldParams, -> extends IFormElement { +export interface IFormEventElement + extends IFormElement { formattedValueDestination: string; formattedId: string; } diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx index ca527b8c5a..10ea4280b9 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx @@ -10,7 +10,7 @@ export interface IFieldLayoutBaseParams { label?: string; } -export const FieldLayout: TDynamicFormField = ({ element, children }) => { +export const FieldLayout: TDynamicFormField = ({ element, children }) => { const { values } = useDynamicForm(); const { stack } = useStack(); const { id, hidden } = useElement(element, stack); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts index d340942635..cf25c8a21d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts @@ -4,6 +4,7 @@ import { CheckboxListField } from '../fields/CheckboxList'; import { DateField } from '../fields/DateField'; import { FieldList } from '../fields/FieldList'; import { MultiselectField } from '../fields/MultiselectField'; +import { SelectField } from '../fields/SelectField'; import { TextField } from '../fields/TextField'; import { TDynamicFormField } from '../types'; @@ -15,6 +16,7 @@ export const baseFields = { multiselectfield: MultiselectField, textfield: TextField, fieldlist: FieldList, + selectfield: SelectField, } as const; export type TBaseFields = keyof typeof baseFields & string; @@ -28,7 +30,7 @@ export const getField = (fieldType: T) }; export const extendFieldsRepository = ( - fields: Record>, + fields: Record>, ) => { const updatedRepository = { ...fieldsRepository, ...fields }; fieldsRepository = updatedRepository; @@ -40,5 +42,5 @@ export const getFieldsRepository = < TElements extends string = TBaseFields, TParams = unknown, >() => { - return fieldsRepository as Record>; + return fieldsRepository as Record>; }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts index 51b5779c19..673ddd2333 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts @@ -35,11 +35,8 @@ export type TDynamicFormElement< element: IFormElement; }>; -export type TDynamicFormField< - TElements extends string = string, - TParams = ICommonFieldParams, -> = FunctionComponent<{ - element: IFormElement; +export type TDynamicFormField = FunctionComponent<{ + element: IFormElement; children?: React.ReactNode | React.ReactNode[]; }>; diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts index f1c515af7b..59186a71eb 100644 --- a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts @@ -423,7 +423,7 @@ describe('useValidate', () => { rerender(); - await vi.runAllTimersAsync(); + await vi.advanceTimersByTimeAsync(550); expect(result.current.errors).toEqual([]); }); From fbc135aa6a259b788975d71856131d8a226adbf3 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Wed, 18 Dec 2024 15:34:12 +0200 Subject: [PATCH 20/54] feat: added initial demo & bugfixes & updated tests --- .../AutocompleteInput/AutocompleteInput.tsx | 3 + .../Form/DynamicForm/DynamicForm.stories.tsx | 10 ++ .../InputsShowcase/InputsShowcase.tsx | 144 +++++++++++++++ .../_stories/InputsShowcase/index.ts | 1 + .../controls/SubmitButton/SubmitButton.tsx | 17 +- .../SubmitButton/SubmitButton.unit.test.tsx | 70 +++++--- .../AutocompleteField/AutocompleteField.tsx | 3 + .../AutocompleteField.unit.test.tsx | 8 +- .../fields/CheckboxField/CheckboxField.tsx | 23 ++- .../CheckboxField/CheckboxField.unit.test.tsx | 43 ++++- .../CheckboxList/CheckboxList.unit.test.tsx | 7 + .../fields/DateField/DateField.tsx | 2 + .../fields/DateField/DateField.unit.test.tsx | 7 + .../MultiselectField/MultiselectField.tsx | 23 ++- .../MultiselectField.unit.test.tsx | 8 + .../fields/SelectField/SelectField.tsx | 34 ++-- .../SelectField/SelectField.unit.test.tsx | 1 - .../fields/TextField/TextField.tsx | 4 +- .../fields/TextField/TextField.unit.test.tsx | 60 +++++-- .../hooks/external/useElement/useElement.ts | 2 +- .../useElement/useElement.unit.test.ts | 169 +++++++----------- .../internal/useEvents/useEvents.unit.test.ts | 6 +- .../hooks/internal/useFieldHelpers/types.ts | 1 + .../useFieldHelpers/useFieldHelpers.ts | 5 +- .../useFieldHelpers.unit.test.ts | 11 ++ .../hooks/internal/useValues/useValues.ts | 3 +- .../internal/useValues/useValues.unit.test.ts | 5 +- .../layouts/FieldErrors/FieldErrors.tsx | 12 +- .../FieldErrors/FieldErrors.unit.test.tsx | 40 ++++- .../layouts/FieldLayout/FieldLayout.tsx | 48 +++-- .../FieldLayout/FieldLayout.unit.test.tsx | 99 ++++++++-- .../repositories/fields-repository.ts | 2 + .../organisms/Form/DynamicForm/types/index.ts | 8 +- .../Form/hooks/useRuleEngine/useRuleEngine.ts | 4 +- .../organisms/Renderer/Renderer.tsx | 4 +- .../organisms/Renderer/Renderer.unit.test.tsx | 18 +- .../components/organisms/Renderer/types.ts | 4 +- 37 files changed, 644 insertions(+), 265 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/index.ts diff --git a/packages/ui/src/components/molecules/inputs/AutocompleteInput/AutocompleteInput.tsx b/packages/ui/src/components/molecules/inputs/AutocompleteInput/AutocompleteInput.tsx index b4ebeaa41e..8529e11ae3 100644 --- a/packages/ui/src/components/molecules/inputs/AutocompleteInput/AutocompleteInput.tsx +++ b/packages/ui/src/components/molecules/inputs/AutocompleteInput/AutocompleteInput.tsx @@ -16,6 +16,7 @@ export type AutocompleteChangeEvent = React.ChangeEvent<{ }>; export interface AutocompleteInputProps { + id?: string; value?: string; options: AutocompleteOption[]; placeholder?: string; @@ -29,6 +30,7 @@ export interface AutocompleteInputProps { } export const AutocompleteInput = ({ + id, options, value = '', placeholder, @@ -75,6 +77,7 @@ export const AutocompleteInput = ({ return ( label} diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx new file mode 100644 index 0000000000..e9453c8dfc --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx @@ -0,0 +1,10 @@ +import { DynamicFormV2 } from './DynamicForm'; +import { InputsShowcaseComponent } from './_stories/InputsShowcase/InputsShowcase'; + +export default { + component: DynamicFormV2, +}; + +export const InputsShowcase = { + render: () => , +}; 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 new file mode 100644 index 0000000000..dd18ac25d1 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx @@ -0,0 +1,144 @@ +import { AnyObject } from '@/common'; +import { useState } from 'react'; +import { JSONEditorComponent } from '../../../Validator/_stories/components/JsonEditor/JsonEditor'; +import { DynamicFormV2 } from '../../DynamicForm'; +import { IFormElement } from '../../types'; + +const schema: Array> = [ + { + id: 'TextField', + element: 'textfield', + valueDestination: 'textfield', + params: { + label: 'Text Field', + placeholder: 'Enter text', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'Text field value is required', + }, + ], + }, + { + id: 'AutocompleteField', + element: 'autocompletefield', + valueDestination: 'autocomplete', + params: { + label: 'Autocomplete Field', + placeholder: 'Select an option', + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, + ], + }, + }, + { + id: 'CheckboxListField', + element: 'checkboxlistfield', + valueDestination: 'checkboxlist', + params: { + label: 'Checkbox List Field', + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, + ], + }, + }, + { + id: 'DateField', + element: 'datefield', + valueDestination: 'date', + params: { + label: 'Date Field', + }, + }, + { + id: 'MultiselectField', + element: 'multiselectfield', + valueDestination: 'multiselect', + params: { + label: 'Multiselect Field', + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, + ], + }, + }, + { + id: 'SelectField', + element: 'selectfield', + valueDestination: 'select', + params: { + label: 'Select Field', + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, + ], + }, + }, + { + id: 'CheckboxField', + element: 'checkboxfield', + valueDestination: 'checkbox', + params: { + label: 'Checkbox Field', + }, + }, + { + id: 'FieldList', + element: 'fieldlist', + valueDestination: 'fieldlist', + params: { + label: 'Field List', + }, + children: [ + { + id: 'Nested-TextField', + element: 'textfield', + valueDestination: 'textfield[$0]', + params: { + label: 'Text Field', + placeholder: 'Enter text', + }, + validate: [], + }, + ], + }, + { + id: 'SubmitButton', + element: 'submitbutton', + valueDestination: 'submitbutton', + params: { + label: 'Submit Button', + }, + }, +]; + +export const InputsShowcaseComponent = () => { + const [context, setContext] = useState({}); + + return ( +
+
+ { + console.log('onSubmit'); + }} + onChange={setContext} + onEvent={console.log} + /> +
+
+ +
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/index.ts new file mode 100644 index 0000000000..79e7592783 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/index.ts @@ -0,0 +1 @@ +export * from './InputsShowcase'; 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 ba1f050433..32c17997d6 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 @@ -1,5 +1,5 @@ import { Button } from '@/components/atoms'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useValidator } from '../../../Validator'; import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external/useElement'; @@ -14,7 +14,10 @@ export interface ISubmitButtonParams { export const SubmitButton: TDynamicFormElement = ({ element }) => { const { id } = useElement(element); const { disabled: _disabled } = useField(element); - const { submit } = useDynamicForm(); + const { fieldHelpers, submit } = useDynamicForm(); + + const { touchAllFields } = fieldHelpers; + const { isValid } = useValidator(); const { disableWhenFormIsInvalid = false, text = 'Submit' } = element.params || {}; @@ -25,12 +28,20 @@ export const SubmitButton: TDynamicFormElement = ({ return _disabled; }, [disableWhenFormIsInvalid, isValid, _disabled]); + const handleSubmit = useCallback(() => { + touchAllFields(); + + if (!isValid) return; + + submit(); + }, [submit, isValid, touchAllFields]); + return ( diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx index 75bdda788d..59834ede96 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx @@ -7,6 +7,7 @@ import { useValidator } from '../../../Validator'; import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external'; import { useField } from '../../hooks/external/useField'; +import { useFieldHelpers } from '../../hooks/internal/useFieldHelpers'; import { IFormElement } from '../../types'; import { ISubmitButtonParams, SubmitButton } from './SubmitButton'; @@ -20,11 +21,10 @@ vi.mock('../../../Validator', () => ({ useValidator: vi.fn(), })); -vi.mock('../../context', () => ({ - useDynamicForm: vi.fn(), -})); +vi.mock('../../context'); vi.mock('../../hooks/external/useField'); +vi.mock('../../hooks/internal/useFieldHelpers'); describe('SubmitButton', () => { const mockElement = { @@ -38,17 +38,37 @@ describe('SubmitButton', () => { beforeEach(() => { cleanup(); - // vi.restoreAllMocks(); + vi.clearAllMocks(); + + const mockSubmit = vi.fn(); vi.mocked(Button).mockImplementation(({ children, ...props }) => ( - + )); + vi.mocked(useFieldHelpers).mockReturnValue({ + touchAllFields: vi.fn(), + } as any); + vi.mocked(useDynamicForm).mockReturnValue({ + submit: mockSubmit, + fieldHelpers: { + touchAllFields: vi.fn(), + }, + } as any); + vi.mocked(useField).mockReturnValue({ disabled: false } as any); - vi.mocked(useDynamicForm).mockReturnValue({ submit: mockSubmit } as any); + vi.mocked(useValidator).mockReturnValue({ isValid: true } as any); vi.mocked(useElement).mockReturnValue({ id: 'test-id' } as any); - vi.mocked(useField).mockReturnValue({ disabled: false } as any); }); afterEach(() => { @@ -57,8 +77,7 @@ describe('SubmitButton', () => { it('should render button with default text', () => { render(); - - screen.getByText('Submit'); + expect(screen.getByText('Submit')).toBeInTheDocument(); }); it('should render button with custom text', () => { @@ -68,16 +87,7 @@ describe('SubmitButton', () => { }; render(); - - screen.getByText('Custom Submit'); - }); - - it('should call submit when clicked', async () => { - render(); - - await userEvent.click(screen.getByRole('button')); - - expect(mockSubmit).toHaveBeenCalled(); + expect(screen.getByText('Custom Submit')).toBeInTheDocument(); }); describe('disabled state', () => { @@ -110,19 +120,29 @@ describe('SubmitButton', () => { expect(screen.getByRole('button')).not.toBeDisabled(); }); + + it('should not call submit when form is invalid and disableWhenFormIsInvalid is true', async () => { + vi.mocked(useValidator).mockReturnValue({ isValid: false } as any); + + const elementWithDisable = { + ...mockElement, + params: { disableWhenFormIsInvalid: true }, + }; + + render(); + await userEvent.click(screen.getByRole('button')); + + expect(mockSubmit).not.toHaveBeenCalled(); + }); }); it('should have correct test id', () => { render(); - expect(screen.getByTestId('test-id-submit-button')).toBeInTheDocument(); }); - it('shold call submit when clicked', async () => { + it('should render with secondary variant', () => { render(); - - await userEvent.click(screen.getByRole('button')); - - expect(mockSubmit).toHaveBeenCalled(); + expect(vi.mocked(Button).mock.calls[0]?.[0]?.variant).toBe('secondary'); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx index 9db014bbf9..e50d7f486b 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx @@ -1,5 +1,6 @@ import { AutocompleteInput } from '@/components/molecules'; import { createTestId } from '@/components/organisms/Renderer'; +import { useElement } from '../../hooks/external'; import { useField } from '../../hooks/external/useField'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -18,6 +19,7 @@ export interface IAutocompleteFieldParams { export const AutocompleteField: TDynamicFormField = ({ element }) => { const { params } = element; const { stack } = useStack(); + const { id } = useElement(element, stack); const { value, onChange, onBlur, onFocus, disabled } = useField( element, stack, @@ -27,6 +29,7 @@ export const AutocompleteField: TDynamicFormField = ({ return ( ({ { - console.log('FOCUS'); onFocus?.(e); }} data-options={JSON.stringify(options)} @@ -39,6 +39,8 @@ vi.mock('../../layouts/FieldLayout', () => ({ FieldLayout: ({ children }: any) =>
{children}
, })); +vi.mock('../../hooks/internal/useEvents'); + describe('AutocompleteField', () => { const mockStack = [0]; const mockOptions = [ @@ -69,6 +71,10 @@ describe('AutocompleteField', () => { vi.mocked(useStack).mockReturnValue({ stack: mockStack }); vi.mocked(useField).mockReturnValue(mockFieldProps); vi.mocked(createTestId).mockReturnValue('test-id'); + vi.mocked(useEvents).mockReturnValue({ + sendEvent: vi.fn(), + sendEventAsync: vi.fn(), + } as unknown as ReturnType); }); it('should render AutocompleteInput component', () => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx index d3a0833f0a..0776efbf80 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx @@ -1,22 +1,29 @@ import { Checkbox } from '@/components/atoms'; -import { useField } from '../../hooks/external'; +import { useElement, useField } from '../../hooks/external'; +import { FieldErrors } from '../../layouts/FieldErrors'; +import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; export const CheckboxField: TDynamicFormField = ({ element }) => { const { stack } = useStack(); + const { id } = useElement(element, stack); const { value, onChange, onFocus, onBlur, disabled } = useField( element, stack, ); return ( - + + + + ); }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx index a945964839..2f2f09ff96 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx @@ -1,7 +1,8 @@ -import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useField } from '../../hooks/external'; +import { useElement, useField } from '../../hooks/external'; +import { FieldLayout } from '../../layouts/FieldLayout'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { CheckboxField } from './CheckboxField'; @@ -12,11 +13,12 @@ vi.mock('@/components/atoms', () => ({ props.onChange(e.target.checked)} + onChange={e => props.onCheckedChange(e.target.checked)} disabled={props.disabled} onFocus={props.onFocus} onBlur={props.onBlur} data-testid="test-checkbox" + id={props.id} /> )), })); @@ -29,12 +31,24 @@ vi.mock('../../hooks/external/useField', () => ({ useField: vi.fn(), })); +vi.mock('../../hooks/external/useElement', () => ({ + useElement: vi.fn(), +})); + +vi.mock('../../layouts/FieldLayout', () => ({ + FieldLayout: vi.fn(({ children }) =>
{children}
), +})); + +vi.mock('../../layouts/FieldErrors', () => ({ + FieldErrors: vi.fn(() =>
), +})); + describe('CheckboxField', () => { const mockStack = [0]; const mockElement = { id: 'test-checkbox', type: '', - } as unknown as IFormElement; + } as unknown as IFormElement; const mockFieldProps = { value: false, @@ -49,6 +63,9 @@ describe('CheckboxField', () => { vi.clearAllMocks(); vi.mocked(useStack).mockReturnValue({ stack: mockStack }); vi.mocked(useField).mockReturnValue(mockFieldProps); + vi.mocked(useElement).mockReturnValue({ id: 'test-checkbox-id' } as unknown as ReturnType< + typeof useElement + >); }); it('renders checkbox with correct initial state', () => { @@ -56,6 +73,20 @@ describe('CheckboxField', () => { const checkbox = screen.getByTestId('test-checkbox'); expect(checkbox).toBeInTheDocument(); expect(checkbox).not.toBeChecked(); + expect(checkbox).toHaveAttribute('id', 'test-checkbox-id'); + }); + + it('renders field layout and errors', () => { + render(); + expect(screen.getByTestId('field-layout')).toBeInTheDocument(); + expect(screen.getByTestId('field-errors')).toBeInTheDocument(); + expect(vi.mocked(FieldLayout)).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + layout: 'horizontal', + }), + expect.any(Object), + ); }); it('renders checked checkbox when value is true', () => { @@ -68,7 +99,7 @@ describe('CheckboxField', () => { expect(screen.getByTestId('test-checkbox')).toBeChecked(); }); - it('handles onChange events', () => { + it('handles onChange events', async () => { const mockOnChange = vi.fn(); vi.mocked(useField).mockReturnValue({ ...mockFieldProps, @@ -78,7 +109,7 @@ describe('CheckboxField', () => { render(); const checkbox = screen.getByTestId('test-checkbox'); - fireEvent.click(checkbox); + await userEvent.click(checkbox); expect(mockOnChange).toHaveBeenCalledWith(true); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx index 8887f97e34..eaf91a9822 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx @@ -2,6 +2,7 @@ import { createTestId } from '@/components/organisms/Renderer'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external/useField'; +import { useEvents } from '../../hooks/internal/useEvents'; import { IFormElement } from '../../types'; import { CheckboxListField, ICheckboxListFieldParams } from './CheckboxList'; @@ -36,6 +37,7 @@ vi.mock('@/components/atoms', () => ({ vi.mock('../../hooks/external/useField', () => ({ useField: vi.fn(), })); +vi.mock('../../hooks/internal/useEvents'); describe('CheckboxListField', () => { const mockOptions = [ @@ -65,6 +67,11 @@ describe('CheckboxListField', () => { onBlur: vi.fn(), disabled: false, } as unknown as ReturnType); + + vi.mocked(useEvents).mockReturnValue({ + sendEvent: vi.fn(), + sendEventAsync: vi.fn(), + } as unknown as ReturnType); }); it('renders all checkbox options', () => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx index 3b36f7102b..b11472eb4d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx @@ -7,6 +7,7 @@ import { import { createTestId } from '@/components/organisms/Renderer'; import { useCallback } from 'react'; import { useField } from '../../hooks/external/useField'; +import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; @@ -58,6 +59,7 @@ export const DateField: TDynamicFormField = ({ element }) => { onChange={handleChange} onFocus={onFocus} /> + ); }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx index 5d1a7c2e3d..e5d62bee3a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx @@ -3,6 +3,7 @@ import { DatePickerInput } from '@/components/molecules'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external/useField'; +import { useEvents } from '../../hooks/internal/useEvents'; import { IFormElement } from '../../types'; import { DateField, IDateFieldParams } from './DateField'; @@ -43,6 +44,7 @@ vi.mock('../../hooks/external/useField'); vi.mock('@/common/utils/check-if-date-is-valid', () => ({ checkIfDateIsValid: vi.fn(), })); +vi.mock('../../hooks/internal/useEvents'); describe('DateField', () => { beforeEach(() => { @@ -57,6 +59,11 @@ describe('DateField', () => { onFocus: vi.fn(), disabled: false, }); + + vi.mocked(useEvents).mockReturnValue({ + sendEvent: vi.fn(), + sendEventAsync: vi.fn(), + } as unknown as ReturnType); }); const mockElement = { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx index e0a054bfde..55da149cba 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx @@ -2,6 +2,8 @@ import { MultiSelect, MultiSelectOption, MultiSelectValue } from '@/components/m import { SelectedElementParams } from '@/components/molecules/inputs/MultiSelect/types'; import { useCallback } from 'react'; import { useField } from '../../hooks/external'; +import { FieldErrors } from '../../layouts/FieldErrors'; +import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { MultiselectfieldSelectedItem } from './MultiselectFieldSelectedItem'; @@ -22,14 +24,17 @@ export const MultiselectField: TDynamicFormField = ({ e }, []); return ( - + + + + ); }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx index 328bde8195..93aa42c269 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx @@ -3,6 +3,7 @@ import { SelectedElementParams } from '@/components/molecules/inputs/MultiSelect import { fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; +import { useEvents } from '../../hooks/internal/useEvents'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { IMultiselectFieldParams, MultiselectField } from './MultiselectField'; @@ -39,6 +40,8 @@ vi.mock('../FieldList/providers/StackProvider', () => ({ useStack: vi.fn(), })); +vi.mock('../../hooks/internal/useEvents'); + describe('MultiselectField', () => { const mockOptions: MultiSelectOption[] = [ { title: 'Option 1', value: 'opt1' }, @@ -68,6 +71,11 @@ describe('MultiselectField', () => { onFocus: vi.fn(), disabled: false, } as unknown as ReturnType); + + vi.mocked(useEvents).mockReturnValue({ + sendEvent: vi.fn(), + sendEventAsync: vi.fn(), + } as unknown as ReturnType); }); it('renders MultiSelect component', () => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx index 6ad40604b6..e4fffb29f8 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx @@ -1,7 +1,8 @@ import { DropdownInput } from '@/components/molecules'; import { createTestId } from '@/components/organisms/Renderer'; import { useElement, useField } from '../../hooks/external'; -import { IFieldLayoutBaseParams } from '../../layouts/FieldLayout'; +import { FieldErrors } from '../../layouts/FieldErrors'; +import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; @@ -10,7 +11,7 @@ export interface ISelectOption { label: string; } -export interface ISelectFieldParams extends IFieldLayoutBaseParams { +export interface ISelectFieldParams { placeholder?: string; options: ISelectOption[]; } @@ -26,18 +27,21 @@ export const SelectField: TDynamicFormField = ({ element }) const { placeholder, options = [] } = element.params || {}; return ( - + + + + ); }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx index 92af5df9ae..15379556c1 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx @@ -14,7 +14,6 @@ vi.mock('@/components/molecules', () => ({ { + const file = e.target.files?.[0]; + + if (file) { + onChange(file); + } + }} + onBlur={onBlur} + onFocus={onFocus} + ref={inputRef} + className="hidden" + /> +
+ +
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx new file mode 100644 index 0000000000..92e29032db --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx @@ -0,0 +1,140 @@ +import { createTestId } from '@/components/organisms/Renderer'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useElement, useField } from '../../hooks/external'; +import { IFormElement } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; +import { FileField, IFileFieldParams } from './FileField'; + +vi.mock('../FieldList/providers/StackProvider'); +vi.mock('../../hooks/external'); +vi.mock('@/components/organisms/Renderer'); + +const mockUseStack = vi.mocked(useStack); +const mockUseElement = vi.mocked(useElement); +const mockUseField = vi.mocked(useField); +const mockCreateTestId = vi.mocked(createTestId); + +describe('FileField', () => { + const mockElement = { + id: 'test-file', + params: { + placeholder: 'Test Placeholder', + acceptFileFormats: '.pdf,.doc', + }, + } as IFormElement; + + const mockOnChange = vi.fn(); + const mockOnBlur = vi.fn(); + const mockOnFocus = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + + mockUseStack.mockReturnValue({ stack: [] }); + mockUseElement.mockReturnValue({ id: 'test-id' } as ReturnType); + mockUseField.mockReturnValue({ + value: undefined, + touched: false, + disabled: false, + onChange: mockOnChange, + onBlur: mockOnBlur, + onFocus: mockOnFocus, + }); + mockCreateTestId.mockReturnValue('test-file-field'); + }); + + it('renders with default state', () => { + render(); + + expect(screen.getByText('Test Placeholder')).toBeInTheDocument(); + expect(screen.getByText('No File Choosen')).toBeInTheDocument(); + }); + + it('handles file selection', () => { + render(); + + const file = new File(['test'], 'test.pdf', { type: 'application/pdf' }); + const input = screen.getByTestId('test-file-field-hidden-input'); + + fireEvent.change(input, { target: { files: [file] } }); + + expect(mockOnChange).toHaveBeenCalledWith(file); + }); + + it('displays file name when file is selected', () => { + mockUseField.mockReturnValue({ + value: new File(['test'], 'test.pdf'), + touched: false, + disabled: false, + onChange: mockOnChange, + onBlur: mockOnBlur, + onFocus: mockOnFocus, + } as ReturnType); + + render(); + + expect(screen.getByText('test.pdf')).toBeInTheDocument(); + }); + + it('handles file removal', () => { + mockUseField.mockReturnValue({ + value: new File(['test'], 'test.pdf'), + touched: false, + disabled: false, + onChange: mockOnChange, + onBlur: mockOnBlur, + onFocus: mockOnFocus, + } as ReturnType); + + render(); + + const removeButton = screen.getByRole('button'); + fireEvent.click(removeButton); + + expect(mockOnChange).toHaveBeenCalledWith(undefined); + }); + + it('applies disabled state correctly', () => { + mockUseField.mockReturnValue({ + value: undefined, + disabled: true, + onChange: mockOnChange, + onBlur: mockOnBlur, + onFocus: mockOnFocus, + touched: false, + } as ReturnType); + + render(); + + const container = screen.getByTestId('test-file-field'); + expect(container).toHaveClass('pointer-events-none opacity-50'); + }); + + it('handles string value by creating a File object', () => { + mockUseField.mockReturnValue({ + value: 'test-file.pdf', + disabled: false, + onChange: mockOnChange, + onBlur: mockOnBlur, + onFocus: mockOnFocus, + touched: false, + } as ReturnType); + + render(); + + expect(screen.getByText('test-file.pdf')).toBeInTheDocument(); + }); + + it('triggers focus and blur events', () => { + render(); + + const input = screen.getByTestId('test-file-field-hidden-input'); + + fireEvent.focus(input); + expect(mockOnFocus).toHaveBeenCalled(); + + fireEvent.blur(input); + expect(mockOnBlur).toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/index.ts new file mode 100644 index 0000000000..18943ce2de --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/index.ts @@ -0,0 +1 @@ +export * from './FileField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx index d25cc40899..799f6191f4 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx @@ -21,6 +21,10 @@ export const TextField: TDynamicFormField = ({ element }) => { const { stack } = useStack(); + if (stack?.length) { + console.log('stack', stack, element); + } + const { id } = useElement(element, stack); const { value, onChange, onBlur, onFocus, disabled } = useField(element, stack); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts index a49f61746b..6f35812e06 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts @@ -13,6 +13,7 @@ export const useField = ( ) => { const fieldId = useElementId(element, stack); const valueDestination = useValueDestination(element, stack); + const { fieldHelpers, values } = useDynamicForm(); const { sendEvent, sendEventAsync } = useEvents(element); const { setValue, getValue, setTouched, getTouched } = fieldHelpers; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts index 63b92fdfab..7466dc482d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts @@ -4,6 +4,7 @@ import { CheckboxField } from '../fields/CheckboxField'; import { CheckboxListField } from '../fields/CheckboxList'; import { DateField } from '../fields/DateField'; import { FieldList } from '../fields/FieldList'; +import { FileField } from '../fields/FileField'; import { MultiselectField } from '../fields/MultiselectField'; import { PhoneField } from '../fields/PhoneField'; import { SelectField } from '../fields/SelectField'; @@ -21,6 +22,7 @@ export const baseFields = { selectfield: SelectField, submitbutton: SubmitButton, phonefield: PhoneField, + filefield: FileField, } as const; export type TBaseFields = keyof typeof baseFields & string; From a8e111ad7d53966b72aac99991fe81d58f984566 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Thu, 19 Dec 2024 12:28:59 +0200 Subject: [PATCH 23/54] feat: added clear value on input hide & updates tests --- .../fields/FieldList/FieldList.unit.test.tsx | 13 ++++ .../hooks/useClearValueOnUnmount/index.ts | 1 + .../useClearValueOnUnmount.ts | 17 +++++ .../useClearValueOnUnmount.unit.test.ts | 66 +++++++++++++++++++ .../hooks/external/useElement/useElement.ts | 2 + .../useElement/useElement.unit.test.ts | 49 ++++++++++---- .../hooks/external/useField/useField.ts | 6 +- .../external/useField/useField.unit.test.ts | 18 ++++- 8 files changed, 157 insertions(+), 15 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/useClearValueOnUnmount.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/useClearValueOnUnmount.unit.test.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx index 4c271293f9..6ad51386bd 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx @@ -2,6 +2,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external'; +import { useEvents } from '../../hooks/internal/useEvents'; import { IFormElement } from '../../types'; import { FieldList } from './FieldList'; import { IUseFieldParams, useFieldList } from './hooks/useFieldList'; @@ -20,6 +21,7 @@ vi.mock('@/components/organisms/Renderer', () => ({ Renderer: () =>
Renderer
, })); +vi.mock('@/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents'); describe('FieldList', () => { const mockElement = { id: 'test-field', @@ -45,6 +47,12 @@ describe('FieldList', () => { vi.mocked(useDynamicForm).mockReturnValue({ elementsMap: {}, + fieldHelpers: { + getValue: vi.fn(), + setValue: vi.fn(), + clearValue: vi.fn(), + getTouched: vi.fn(), + }, } as any); vi.mocked(useStack).mockReturnValue({ @@ -58,6 +66,11 @@ describe('FieldList', () => { addItem: mockAddItem, removeItem: mockRemoveItem, }); + + vi.mocked(useEvents).mockReturnValue({ + sendEvent: vi.fn(), + sendEventAsync: vi.fn(), + } as unknown as ReturnType); }); describe('test ids', () => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/index.ts new file mode 100644 index 0000000000..ff7a263fd3 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/index.ts @@ -0,0 +1 @@ +export * from './useClearValueOnUnmount'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/useClearValueOnUnmount.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/useClearValueOnUnmount.ts new file mode 100644 index 0000000000..0f253e4b5e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/useClearValueOnUnmount.ts @@ -0,0 +1,17 @@ +import { useStack } from '@/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider'; +import { IFormElement } from '@/components/organisms/Form/DynamicForm/types'; +import { useRef } from 'react'; +import { useUnmount } from '../../../../internal/useUnmount'; +import { useField } from '../../../useField'; + +export const useClearValueOnUnmount = (element: IFormElement, hidden: boolean) => { + const { stack } = useStack(); + const { onChange } = useField(element, stack); + const prevHidden = useRef(hidden); + + useUnmount(() => { + if (!prevHidden.current && hidden) { + onChange(undefined, true); + } + }); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/useClearValueOnUnmount.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/useClearValueOnUnmount.unit.test.ts new file mode 100644 index 0000000000..c0ef3fbc9e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/hooks/useClearValueOnUnmount/useClearValueOnUnmount.unit.test.ts @@ -0,0 +1,66 @@ +import { useStack } from '@/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider'; +import { IFormElement } from '@/components/organisms/Form/DynamicForm/types'; +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useUnmount } from '../../../../internal/useUnmount'; +import { useField } from '../../../useField'; +import { useClearValueOnUnmount } from './useClearValueOnUnmount'; + +vi.mock('@/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider'); +vi.mock('../../../useField'); +vi.mock('../../../../internal/useUnmount'); + +describe('useClearValueOnUnmount', () => { + const mockElement = { + id: 'test-element', + } as IFormElement; + + const mockOnChange = vi.fn(); + const mockUnmountCallback = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(useStack).mockReturnValue({ stack: [] }); + vi.mocked(useField).mockReturnValue({ onChange: mockOnChange } as any); + vi.mocked(useUnmount).mockImplementation(callback => { + mockUnmountCallback.mockImplementation(callback); + }); + }); + + it('should not clear value when hidden state has not changed', () => { + renderHook(() => useClearValueOnUnmount(mockElement, false)); + + mockUnmountCallback(); + + expect(mockOnChange).not.toHaveBeenCalled(); + }); + + it('should not clear value when element was already hidden', () => { + renderHook(() => useClearValueOnUnmount(mockElement, true)); + + mockUnmountCallback(); + + expect(mockOnChange).not.toHaveBeenCalled(); + }); + + it('should clear value when element becomes hidden', () => { + const { rerender } = renderHook(({ hidden }) => useClearValueOnUnmount(mockElement, hidden), { + initialProps: { hidden: false }, + }); + + rerender({ hidden: true }); + mockUnmountCallback(); + + expect(mockOnChange).toHaveBeenCalledWith(undefined, true); + }); + + it('should use stack from useStack hook', () => { + const mockStack = [1, 2, 3]; + vi.mocked(useStack).mockReturnValue({ stack: mockStack }); + + renderHook(() => useClearValueOnUnmount(mockElement, false)); + + expect(useField).toHaveBeenCalledWith(mockElement, mockStack); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts index 64cb17cbd0..10704ae534 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts @@ -7,6 +7,7 @@ import { useEvents } from '../../internal/useEvents'; import { useMount } from '../../internal/useMount'; import { useUnmount } from '../../internal/useUnmount'; import { useElementId } from '../useElementId'; +import { useClearValueOnUnmount } from './hooks/useClearValueOnUnmount'; export const useElement = ( element: IFormElement, @@ -28,6 +29,7 @@ export const useElement = ( useMount(() => sendEvent('onMount')); useUnmount(() => sendEvent('onUnmount')); + useClearValueOnUnmount(element, isHidden); return { id: useElementId(element, stack), diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts index 77abee6350..b65f0c05f6 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts @@ -1,4 +1,7 @@ -import { IRuleExecutionResult, useRuleEngine } from '@/components/organisms/Form/hooks'; +import { + IRuleExecutionResult, + useRuleEngine, +} from '@/components/organisms/Form/hooks/useRuleEngine'; import { renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useDynamicForm } from '../../../context'; @@ -7,6 +10,7 @@ import { useEvents } from '../../internal/useEvents'; import { useMount } from '../../internal/useMount'; import { useUnmount } from '../../internal/useUnmount'; import { useElementId } from '../useElementId'; +import { useClearValueOnUnmount } from './hooks/useClearValueOnUnmount'; import { useElement } from './useElement'; vi.mock('@/components/organisms/Form/hooks/useRuleEngine'); @@ -15,6 +19,7 @@ vi.mock('../../internal/useEvents'); vi.mock('../../internal/useMount'); vi.mock('../../internal/useUnmount'); vi.mock('../useElementId'); +vi.mock('./hooks/useClearValueOnUnmount'); describe('useElement', () => { const mockSendEvent = vi.fn(); @@ -39,11 +44,12 @@ describe('useElement', () => { }); vi.mocked(useRuleEngine).mockReturnValue([]); + vi.mocked(useClearValueOnUnmount).mockImplementation(() => undefined); }); describe('when stack not provided', () => { it('should return unmodified id and origin id', () => { - const element = { id: 'test-id' } as IFormElement; + const element = { id: 'test-id' } as IFormElement; const { result } = renderHook(() => useElement(element)); @@ -54,7 +60,7 @@ describe('useElement', () => { describe('when stack provided', () => { it('should format id with stack', () => { - const element = { id: 'test-id' } as IFormElement; + const element = { id: 'test-id' } as IFormElement; const stack = [1, 2]; const { result } = renderHook(() => useElement(element, stack)); @@ -67,30 +73,38 @@ describe('useElement', () => { describe('when hidden rules provided', () => { it('should return hidden true when all hidden rules return true', () => { vi.mocked(useRuleEngine).mockReturnValue([ - { result: true }, - { result: true }, + { result: true, rule: {} }, + { result: true, rule: {} }, ] as IRuleExecutionResult[]); const element = { id: 'test-id', hidden: [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 1] } }], - } as IFormElement; + } as IFormElement; const { result } = renderHook(() => useElement(element)); expect(result.current.hidden).toBe(true); + expect(useRuleEngine).toHaveBeenCalledWith( + { test: 1 }, + { + rules: element.hidden, + runOnInitialize: true, + executionDelay: 500, + }, + ); }); it('should return hidden false when any hidden rule returns false', () => { vi.mocked(useRuleEngine).mockReturnValue([ - { result: true }, - { result: false }, + { result: true, rule: {} }, + { result: false, rule: {} }, ] as IRuleExecutionResult[]); const element = { id: 'test-id', hidden: [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 5] } }], - } as IFormElement; + } as IFormElement; const { result } = renderHook(() => useElement(element)); @@ -102,7 +116,7 @@ describe('useElement', () => { const element = { id: 'test-id', - } as IFormElement; + } as IFormElement; const { result } = renderHook(() => useElement(element)); @@ -112,7 +126,7 @@ describe('useElement', () => { describe('lifecycle events', () => { it('should call sendEvent with onMount on mount', () => { - const element = { id: 'test-id' } as IFormElement; + const element = { id: 'test-id' } as IFormElement; renderHook(() => useElement(element)); @@ -127,7 +141,7 @@ describe('useElement', () => { }); it('should call sendEvent with onUnmount on unmount', () => { - const element = { id: 'test-id' } as IFormElement; + const element = { id: 'test-id' } as IFormElement; renderHook(() => useElement(element)); @@ -140,5 +154,16 @@ describe('useElement', () => { expect(mockSendEvent).toHaveBeenCalledWith('onUnmount'); }); + + it('should call useClearValueOnUnmount with element and hidden state', () => { + const element = { id: 'test-id' } as IFormElement; + vi.mocked(useRuleEngine).mockReturnValue([ + { result: true, rule: {} }, + ] as IRuleExecutionResult[]); + + renderHook(() => useElement(element)); + + expect(useClearValueOnUnmount).toHaveBeenCalledWith(element, true); + }); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts index 6f35812e06..f008ef8062 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts @@ -34,11 +34,13 @@ export const useField = ( }, [disabledRulesResult]); const onChange = useCallback( - (value: TValue) => { + (value: TValue, ignoreEvent = false) => { setValue(fieldId, valueDestination, value); setTouched(fieldId, true); - sendEventAsync('onChange'); + if (!ignoreEvent) { + sendEventAsync('onChange'); + } }, [fieldId, valueDestination, setValue, setTouched, sendEventAsync], ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts index b60d864bc1..fd764c6e88 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts @@ -1,6 +1,6 @@ import { IRuleExecutionResult, useRuleEngine } from '@/components/organisms/Form/hooks'; import { renderHook } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { IDynamicFormContext, useDynamicForm } from '../../../context'; import { ICommonFieldParams, IFormElement } from '../../../types'; import { useEvents } from '../../internal/useEvents'; @@ -69,6 +69,12 @@ describe('useField', () => { } as unknown as IDynamicFormContext); mockGetValue.mockReturnValue('test-value'); mockGetTouched.mockReturnValue(false); + + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); }); it('should return field state and handlers', () => { @@ -118,6 +124,16 @@ describe('useField', () => { expect(mockSetTouched).toHaveBeenCalledWith('test-field-1-2', true); expect(mockSendEventAsync).toHaveBeenCalledWith('onChange'); }); + + it('should not trigger async event when ignoreEvent is true', () => { + const { result } = renderHook(() => useField(mockElement, mockStack)); + + result.current.onChange('new-value', true); + + vi.advanceTimersByTime(550); + + expect(mockSendEventAsync).not.toHaveBeenCalled(); + }); }); describe('onBlur', () => { From e9e155f9c2e6388a568617d26a652423f2a7d490 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Thu, 19 Dec 2024 14:05:51 +0200 Subject: [PATCH 24/54] feat: fixed onMount & onUnmount events & updated tests --- packages/ui/.gitignore | 1 + .../AutocompleteField/AutocompleteField.tsx | 6 + .../AutocompleteField.unit.test.tsx | 164 ------------------ .../fields/CheckboxField/CheckboxField.tsx | 5 + .../CheckboxField/CheckboxField.unit.test.tsx | 32 ++++ .../fields/CheckboxList/CheckboxList.tsx | 5 + .../CheckboxList/CheckboxList.unit.test.tsx | 78 +++++---- .../fields/DateField/DateField.tsx | 5 + .../fields/DateField/DateField.unit.test.tsx | 31 ++++ .../fields/FieldList/FieldList.tsx | 5 + .../fields/FieldList/FieldList.unit.test.tsx | 31 ++++ .../fields/FileField/FileField.tsx | 5 + .../fields/FileField/FileField.unit.test.tsx | 140 --------------- .../MultiselectField/MultiselectField.tsx | 14 +- .../MultiselectField.unit.test.tsx | 79 ++++++--- .../fields/PhoneField/PhoneField.tsx | 15 +- .../PhoneField/PhoneField.unit.test.tsx | 28 ++- .../fields/SelectField/SelectField.tsx | 15 +- .../SelectField/SelectField.unit.test.tsx | 75 +++++++- .../fields/TextField/TextField.tsx | 9 +- .../fields/TextField/TextField.unit.test.tsx | 57 +++++- .../hooks/external/useElement/useElement.ts | 6 - .../useElement/useElement.unit.test.ts | 34 ---- .../hooks/internal/useMountEvent/index.ts | 1 + .../internal/useMountEvent/useMountEvent.ts | 9 + .../useMountEvent/useMountEvent.unit.test.ts | 43 +++++ .../hooks/internal/useUnmountEvent/index.ts | 1 + .../useUnmountEvent/useUnmountEvent.ts | 9 + .../useUnmountEvent.unit.test.ts | 43 +++++ .../format-value-destination.ts | 2 +- packages/ui/src/main.tsx | 9 + 31 files changed, 546 insertions(+), 411 deletions(-) delete mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx delete mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/useMountEvent.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/useMountEvent.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/useUnmountEvent.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/useUnmountEvent.unit.test.ts create mode 100644 packages/ui/src/main.tsx diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore index c9ce7c7ee4..96fc60452d 100644 --- a/packages/ui/.gitignore +++ b/packages/ui/.gitignore @@ -24,3 +24,4 @@ dist-ssr *.sw? vite.config.ts.timestamp-*.mjs tsconfig.tsbuildinfo +./src/main.tsx diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx index 80e1927803..4ff28ea7ac 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx @@ -7,6 +7,9 @@ import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; + export interface IAutocompleteFieldOption { label: string; value: string; @@ -18,6 +21,9 @@ export interface IAutocompleteFieldParams { } export const AutocompleteField: TDynamicFormField = ({ element }) => { + useMountEvent(element); + useUnmountEvent(element); + const { params } = element; const { stack } = useStack(); const { id } = useElement(element, stack); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx deleted file mode 100644 index 3f574e2146..0000000000 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.unit.test.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { createTestId } from '@/components/organisms/Renderer'; -import { cleanup, render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useField } from '../../hooks/external/useField'; -import { useEvents } from '../../hooks/internal/useEvents'; -import { IFormElement } from '../../types'; -import { useStack } from '../FieldList/providers/StackProvider'; -import { AutocompleteField, IAutocompleteFieldParams } from './AutocompleteField'; - -// Mock dependencies -vi.mock('@/components/molecules', () => ({ - AutocompleteInput: ({ children, options, onFocus, ...props }: any) => ( - { - onFocus?.(e); - }} - data-options={JSON.stringify(options)} - type="text" - tabIndex={0} - /> - ), -})); - -vi.mock('../../hooks/external/useField', () => ({ - useField: vi.fn(), -})); - -vi.mock('../FieldList/providers/StackProvider', () => ({ - useStack: vi.fn(), -})); - -vi.mock('@/components/organisms/Renderer', () => ({ - createTestId: vi.fn(), -})); - -vi.mock('../../layouts/FieldLayout', () => ({ - FieldLayout: ({ children }: any) =>
{children}
, -})); - -vi.mock('../../hooks/internal/useEvents'); - -describe('AutocompleteField', () => { - const mockStack = [0]; - const mockOptions = [ - { label: 'Option 1', value: 'opt1' }, - { label: 'Option 2', value: 'opt2' }, - ]; - - const mockElement = { - id: 'test-autocomplete', - params: { - placeholder: 'Select an option', - options: mockOptions, - }, - } as unknown as IFormElement; - - const mockFieldProps = { - value: '', - onChange: vi.fn(), - onBlur: vi.fn(), - onFocus: vi.fn(), - disabled: false, - touched: false, - } as ReturnType; - - beforeEach(() => { - cleanup(); - vi.clearAllMocks(); - vi.mocked(useStack).mockReturnValue({ stack: mockStack }); - vi.mocked(useField).mockReturnValue(mockFieldProps); - vi.mocked(createTestId).mockReturnValue('test-id'); - vi.mocked(useEvents).mockReturnValue({ - sendEvent: vi.fn(), - sendEventAsync: vi.fn(), - } as unknown as ReturnType); - }); - - it('should render AutocompleteInput component', () => { - render(); - expect(screen.getByTestId('test-id')).toBeInTheDocument(); - }); - - it('should pass correct props to AutocompleteInput', () => { - render(); - const input = screen.getByTestId('test-id'); - - expect(input).toHaveAttribute('placeholder', 'Select an option'); - expect(input).not.toBeDisabled(); - }); - - it('should handle value changes', async () => { - const user = userEvent.setup(); - render(); - - const input = screen.getByTestId('test-id'); - await user.type(input, 'test value'); - - expect(mockFieldProps.onChange).toHaveBeenCalled(); - }); - - it('should handle blur events', async () => { - const user = userEvent.setup(); - render(); - - const input = screen.getByTestId('test-id'); - await user.click(input); - await user.tab(); - - expect(mockFieldProps.onBlur).toHaveBeenCalled(); - }); - - it('should handle focus events', async () => { - const user = userEvent.setup(); - - const { container } = render(); - - const input = container.querySelector('input'); - - await user.click(input!); - console.log(input?.outerHTML); - - expect(mockFieldProps.onFocus).toHaveBeenCalled(); - }); - - it('should respect disabled state', () => { - vi.mocked(useField).mockReturnValue({ - ...mockFieldProps, - disabled: true, - }); - - render(); - expect(screen.getByTestId('test-id')).toBeDisabled(); - }); - - it('should use default params when none provided', () => { - const elementWithoutParams = { - id: 'test-autocomplete', - } as unknown as IFormElement; - - render(); - - const input = screen.getByTestId('test-id'); - expect(input).toHaveAttribute('placeholder', ''); - }); - - it('should pass options from element params to AutocompleteInput', () => { - const elementWithOptions = { - ...mockElement, - params: { - options: mockOptions, - placeholder: 'Select an option', - }, - }; - - render(); - - expect(screen.getByTestId('test-id')).toHaveAttribute( - 'data-options', - JSON.stringify(mockOptions), - ); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx index 0776efbf80..468ddc2488 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx @@ -1,11 +1,16 @@ import { Checkbox } from '@/components/atoms'; import { useElement, useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; export const CheckboxField: TDynamicFormField = ({ element }) => { + useMountEvent(element); + useUnmountEvent(element); + const { stack } = useStack(); const { id } = useElement(element, stack); const { value, onChange, onFocus, onBlur, disabled } = useField( diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx index 2f2f09ff96..3f2afafc92 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx @@ -2,6 +2,9 @@ import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useElement, useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; @@ -43,6 +46,14 @@ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(() =>
), })); +vi.mock('../../hooks/internal/useMountEvent', () => ({ + useMountEvent: vi.fn(), +})); + +vi.mock('../../hooks/internal/useUnmountEvent', () => ({ + useUnmountEvent: vi.fn(), +})); + describe('CheckboxField', () => { const mockStack = [0]; const mockElement = { @@ -154,4 +165,25 @@ describe('CheckboxField', () => { render(); expect(screen.getByTestId('test-checkbox')).not.toBeChecked(); }); + + it('should call useMountEvent with element', () => { + const mockUseMountEvent = vi.mocked(useMountEvent); + render(); + expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should call useUnmountEvent with element', () => { + const mockUseUnmountEvent = vi.mocked(useUnmountEvent); + const { unmount } = render(); + unmount(); + expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should render FieldErrors with element prop', () => { + render(); + expect(FieldErrors).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx index a7f16fc58b..ef1ac01b72 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx @@ -2,6 +2,8 @@ import { ctw } from '@/common'; import { Checkbox } from '@/components/atoms'; import { createTestId } from '@/components/organisms/Renderer'; import { useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -17,6 +19,9 @@ export interface ICheckboxListFieldParams { } export const CheckboxListField: TDynamicFormField = ({ element }) => { + useMountEvent(element); + useUnmountEvent(element); + const { options = [] } = element.params || {}; const { stack } = useStack(); const { value, onChange, onFocus, onBlur, disabled } = useField(element, stack); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx index eaf91a9822..a511257ffa 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx @@ -1,24 +1,36 @@ +import { ctw } from '@/common'; import { createTestId } from '@/components/organisms/Renderer'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useField } from '../../hooks/external/useField'; -import { useEvents } from '../../hooks/internal/useEvents'; +import { useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; import { CheckboxListField, ICheckboxListFieldParams } from './CheckboxList'; // Mock dependencies -vi.mock('@/components/organisms/Renderer'); +vi.mock('@/common', () => ({ + ctw: vi.fn(), +})); + +vi.mock('@/components/organisms/Renderer', () => ({ + createTestId: vi.fn(), +})); vi.mock('../FieldList/providers/StackProvider', () => ({ - useStack: () => ({ - stack: [], - }), + useStack: vi.fn(), })); vi.mock('../../layouts/FieldLayout', () => ({ FieldLayout: ({ children }: { children: React.ReactNode }) =>
{children}
, })); +vi.mock('../../layouts/FieldErrors', () => ({ + FieldErrors: vi.fn(), +})); + vi.mock('@/components/atoms', () => ({ Checkbox: vi.fn((props: any) => ( ({ )), })); -vi.mock('../../hooks/external/useField', () => ({ +vi.mock('../../hooks/external', () => ({ useField: vi.fn(), })); -vi.mock('../../hooks/internal/useEvents'); + +vi.mock('../../hooks/internal/useMountEvent', () => ({ + useMountEvent: vi.fn(), +})); + +vi.mock('../../hooks/internal/useUnmountEvent', () => ({ + useUnmountEvent: vi.fn(), +})); describe('CheckboxListField', () => { const mockOptions = [ @@ -59,6 +78,8 @@ describe('CheckboxListField', () => { vi.clearAllMocks(); vi.mocked(createTestId).mockReturnValue('test-checkbox-list'); + vi.mocked(ctw).mockImplementation((...args: any[]) => args.filter(Boolean).join(' ')); + vi.mocked(useStack).mockReturnValue({ stack: [] }); vi.mocked(useField).mockReturnValue({ value: ['opt1'], @@ -67,11 +88,6 @@ describe('CheckboxListField', () => { onBlur: vi.fn(), disabled: false, } as unknown as ReturnType); - - vi.mocked(useEvents).mockReturnValue({ - sendEvent: vi.fn(), - sendEventAsync: vi.fn(), - } as unknown as ReturnType); }); it('renders all checkbox options', () => { @@ -165,22 +181,6 @@ describe('CheckboxListField', () => { expect(mockOnBlur).toHaveBeenCalled(); }); - it('applies disabled styling when disabled', () => { - vi.mocked(useField).mockReturnValue({ - value: [], - onChange: vi.fn(), - onFocus: vi.fn(), - onBlur: vi.fn(), - disabled: true, - } as unknown as ReturnType); - - render(); - - const container = screen.getByTestId('test-checkbox-list'); - expect(container.className).toContain('pointer-events-none'); - expect(container.className).toContain('opacity-50'); - }); - it('handles empty options array', () => { const emptyElement = { ...mockElement, @@ -191,4 +191,24 @@ describe('CheckboxListField', () => { expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); }); + + it('should call useMountEvent with element', () => { + const mockUseMountEvent = vi.mocked(useMountEvent); + render(); + expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should call useUnmountEvent with element', () => { + const mockUseUnmountEvent = vi.mocked(useUnmountEvent); + render(); + expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should render FieldErrors with element prop', () => { + render(); + expect(FieldErrors).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx index b11472eb4d..bbb97e6c1f 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx @@ -7,6 +7,8 @@ import { import { createTestId } from '@/components/organisms/Renderer'; import { useCallback } from 'react'; import { useField } from '../../hooks/external/useField'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -19,6 +21,9 @@ export interface IDateFieldParams { } export const DateField: TDynamicFormField = ({ element }) => { + useMountEvent(element); + useUnmountEvent(element); + const { disableFuture = false, disablePast = false, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx index e5d62bee3a..2a350aa578 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx @@ -4,6 +4,9 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external/useField'; import { useEvents } from '../../hooks/internal/useEvents'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; import { DateField, IDateFieldParams } from './DateField'; @@ -45,6 +48,11 @@ vi.mock('@/common/utils/check-if-date-is-valid', () => ({ checkIfDateIsValid: vi.fn(), })); vi.mock('../../hooks/internal/useEvents'); +vi.mock('../../hooks/internal/useMountEvent'); +vi.mock('../../hooks/internal/useUnmountEvent'); +vi.mock('../../layouts/FieldErrors', () => ({ + FieldErrors: vi.fn(), +})); describe('DateField', () => { beforeEach(() => { @@ -64,6 +72,9 @@ describe('DateField', () => { sendEvent: vi.fn(), sendEventAsync: vi.fn(), } as unknown as ReturnType); + + vi.mocked(useMountEvent).mockReturnValue(undefined); + vi.mocked(useUnmountEvent).mockReturnValue(undefined); }); const mockElement = { @@ -182,4 +193,24 @@ describe('DateField', () => { expect(dateInput).toBeDisabled(); }); + + it('should call useMountEvent with element', () => { + const mockUseMountEvent = vi.mocked(useMountEvent); + render(); + expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should call useUnmountEvent with element', () => { + const mockUseUnmountEvent = vi.mocked(useUnmountEvent); + render(); + expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should render FieldErrors with element prop', () => { + render(); + expect(FieldErrors).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx index 08265a66f5..d66ade5683 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx @@ -3,6 +3,8 @@ import { Button } from '@/components/atoms'; import { Renderer, TRendererSchema } from '@/components/organisms/Renderer'; import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { TDynamicFormField } from '../../types'; import { useFieldList } from './hooks/useFieldList'; @@ -17,6 +19,9 @@ export interface IFieldListParams { } export const FieldList: TDynamicFormField = props => { + useMountEvent(props.element); + useUnmountEvent(props.element); + const { elementsMap } = useDynamicForm(); const { stack } = useStack(); const { element } = props; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx index 6ad51386bd..11e5af5ad3 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx @@ -3,6 +3,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external'; import { useEvents } from '../../hooks/internal/useEvents'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; import { FieldList } from './FieldList'; import { IUseFieldParams, useFieldList } from './hooks/useFieldList'; @@ -12,6 +15,11 @@ vi.mock('../../context'); vi.mock('../../hooks/external/useElement'); vi.mock('./providers/StackProvider'); vi.mock('./hooks/useFieldList'); +vi.mock('../../hooks/internal/useMountEvent'); +vi.mock('../../hooks/internal/useUnmountEvent'); +vi.mock('../../layouts/FieldErrors', () => ({ + FieldErrors: vi.fn(), +})); vi.mock('@/components/atoms', () => ({ Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => ( @@ -71,6 +79,9 @@ describe('FieldList', () => { sendEvent: vi.fn(), sendEventAsync: vi.fn(), } as unknown as ReturnType); + + vi.mocked(useMountEvent).mockReturnValue(undefined); + vi.mocked(useUnmountEvent).mockReturnValue(undefined); }); describe('test ids', () => { @@ -137,4 +148,24 @@ describe('FieldList', () => { expect(mockRemoveItem).toHaveBeenCalledWith(1); }); + + it('should call useMountEvent with element', () => { + const mockUseMountEvent = vi.mocked(useMountEvent); + render(); + expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should call useUnmountEvent with element', () => { + const mockUseUnmountEvent = vi.mocked(useUnmountEvent); + render(); + expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should render FieldErrors with element prop', () => { + render(); + expect(FieldErrors).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx index a88ea43244..ab4f8ec53a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx @@ -5,6 +5,8 @@ import { createTestId } from '@/components/organisms/Renderer'; import { Upload, XCircle } from 'lucide-react'; import { useCallback, useMemo, useRef } from 'react'; import { useElement, useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { ICommonFieldParams, TDynamicFormField } from '../../types'; @@ -21,6 +23,9 @@ export interface IFileFieldParams extends ICommonFieldParams { } export const FileField: TDynamicFormField = ({ element }) => { + useMountEvent(element); + useUnmountEvent(element); + const { uploadOn = 'change', uploadSettings = {}, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx deleted file mode 100644 index 92e29032db..0000000000 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { createTestId } from '@/components/organisms/Renderer'; -import { fireEvent, render, screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useElement, useField } from '../../hooks/external'; -import { IFormElement } from '../../types'; -import { useStack } from '../FieldList/providers/StackProvider'; -import { FileField, IFileFieldParams } from './FileField'; - -vi.mock('../FieldList/providers/StackProvider'); -vi.mock('../../hooks/external'); -vi.mock('@/components/organisms/Renderer'); - -const mockUseStack = vi.mocked(useStack); -const mockUseElement = vi.mocked(useElement); -const mockUseField = vi.mocked(useField); -const mockCreateTestId = vi.mocked(createTestId); - -describe('FileField', () => { - const mockElement = { - id: 'test-file', - params: { - placeholder: 'Test Placeholder', - acceptFileFormats: '.pdf,.doc', - }, - } as IFormElement; - - const mockOnChange = vi.fn(); - const mockOnBlur = vi.fn(); - const mockOnFocus = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - - mockUseStack.mockReturnValue({ stack: [] }); - mockUseElement.mockReturnValue({ id: 'test-id' } as ReturnType); - mockUseField.mockReturnValue({ - value: undefined, - touched: false, - disabled: false, - onChange: mockOnChange, - onBlur: mockOnBlur, - onFocus: mockOnFocus, - }); - mockCreateTestId.mockReturnValue('test-file-field'); - }); - - it('renders with default state', () => { - render(); - - expect(screen.getByText('Test Placeholder')).toBeInTheDocument(); - expect(screen.getByText('No File Choosen')).toBeInTheDocument(); - }); - - it('handles file selection', () => { - render(); - - const file = new File(['test'], 'test.pdf', { type: 'application/pdf' }); - const input = screen.getByTestId('test-file-field-hidden-input'); - - fireEvent.change(input, { target: { files: [file] } }); - - expect(mockOnChange).toHaveBeenCalledWith(file); - }); - - it('displays file name when file is selected', () => { - mockUseField.mockReturnValue({ - value: new File(['test'], 'test.pdf'), - touched: false, - disabled: false, - onChange: mockOnChange, - onBlur: mockOnBlur, - onFocus: mockOnFocus, - } as ReturnType); - - render(); - - expect(screen.getByText('test.pdf')).toBeInTheDocument(); - }); - - it('handles file removal', () => { - mockUseField.mockReturnValue({ - value: new File(['test'], 'test.pdf'), - touched: false, - disabled: false, - onChange: mockOnChange, - onBlur: mockOnBlur, - onFocus: mockOnFocus, - } as ReturnType); - - render(); - - const removeButton = screen.getByRole('button'); - fireEvent.click(removeButton); - - expect(mockOnChange).toHaveBeenCalledWith(undefined); - }); - - it('applies disabled state correctly', () => { - mockUseField.mockReturnValue({ - value: undefined, - disabled: true, - onChange: mockOnChange, - onBlur: mockOnBlur, - onFocus: mockOnFocus, - touched: false, - } as ReturnType); - - render(); - - const container = screen.getByTestId('test-file-field'); - expect(container).toHaveClass('pointer-events-none opacity-50'); - }); - - it('handles string value by creating a File object', () => { - mockUseField.mockReturnValue({ - value: 'test-file.pdf', - disabled: false, - onChange: mockOnChange, - onBlur: mockOnBlur, - onFocus: mockOnFocus, - touched: false, - } as ReturnType); - - render(); - - expect(screen.getByText('test-file.pdf')).toBeInTheDocument(); - }); - - it('triggers focus and blur events', () => { - render(); - - const input = screen.getByTestId('test-file-field-hidden-input'); - - fireEvent.focus(input); - expect(mockOnFocus).toHaveBeenCalled(); - - fireEvent.blur(input); - expect(mockOnBlur).toHaveBeenCalled(); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx index 55da149cba..e89ea1971a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx @@ -2,6 +2,8 @@ import { MultiSelect, MultiSelectOption, MultiSelectValue } from '@/components/m import { SelectedElementParams } from '@/components/molecules/inputs/MultiSelect/types'; import { useCallback } from 'react'; import { useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -13,6 +15,9 @@ export interface IMultiselectFieldParams { } export const MultiselectField: TDynamicFormField = ({ element }) => { + useMountEvent(element); + useUnmountEvent(element); + const { stack } = useStack(); const { value, onChange, onBlur, onFocus, disabled } = useField( element, @@ -23,12 +28,19 @@ export const MultiselectField: TDynamicFormField = ({ e return ; }, []); + const handleChange = useCallback( + (value: MultiSelectValue[]) => { + onChange(value); + }, + [onChange], + ); + return ( ({ props.onChange(e.target.value, '')} + onChange={e => props.onChange([e.target.value])} onBlur={props.onBlur} onFocus={props.onFocus} /> - {props.value?.map((val: string, idx: number) => ( + {props.value?.map((val: MultiSelectValue, idx: number) => (
- {props.renderSelected({ unselectButtonProps: {} }, { value: val, title: val })} + {props.renderSelected({ unselectButtonProps: {} }, { value: val, title: String(val) })}
))}
)), })); -vi.mock('../../hooks/external/useField', () => ({ +vi.mock('../../hooks/external', () => ({ useField: vi.fn(), })); +vi.mock('../../hooks/internal/useMountEvent', () => ({ + useMountEvent: vi.fn(), +})); + +vi.mock('../../hooks/internal/useUnmountEvent', () => ({ + useUnmountEvent: vi.fn(), +})); + +vi.mock('../../layouts/FieldErrors', () => ({ + FieldErrors: vi.fn(), +})); + +vi.mock('../../layouts/FieldLayout', () => ({ + FieldLayout: vi.fn(({ children }) =>
{children}
), +})); + vi.mock('../FieldList/providers/StackProvider', () => ({ useStack: vi.fn(), })); -vi.mock('../../hooks/internal/useEvents'); - describe('MultiselectField', () => { const mockOptions: MultiSelectOption[] = [ { title: 'Option 1', value: 'opt1' }, @@ -71,16 +89,15 @@ describe('MultiselectField', () => { onFocus: vi.fn(), disabled: false, } as unknown as ReturnType); - - vi.mocked(useEvents).mockReturnValue({ - sendEvent: vi.fn(), - sendEventAsync: vi.fn(), - } as unknown as ReturnType); }); - it('renders MultiSelect component', () => { + it('renders MultiSelect component within FieldLayout', () => { render(); expect(screen.getByTestId('multiselect')).toBeInTheDocument(); + expect(FieldLayout).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); }); it('passes correct props to MultiSelect', () => { @@ -102,7 +119,6 @@ describe('MultiselectField', () => { expect(multiselect.value).toEqual(['opt1']); expect(multiselect.disabled).toBe(false); expect(multiselect.options).toEqual(mockOptions); - expect(multiselect.onChange).toBe(mockOnChange); expect(multiselect.onBlur).toBe(mockOnBlur); expect(multiselect.onFocus).toBe(mockOnFocus); }); @@ -157,7 +173,8 @@ describe('MultiselectField', () => { }); }); - it('handles onBlur events', () => { + it('handles onBlur events', async () => { + const user = userEvent.setup(); const mockOnBlur = vi.fn(); vi.mocked(useField).mockReturnValue({ value: ['opt1'], @@ -170,11 +187,13 @@ describe('MultiselectField', () => { render(); const input = screen.getByTestId('multiselect-input'); - fireEvent.blur(input); + await user.click(input); + await user.tab(); expect(mockOnBlur).toHaveBeenCalled(); }); - it('handles onFocus events', () => { + it('handles onFocus events', async () => { + const user = userEvent.setup(); const mockOnFocus = vi.fn(); vi.mocked(useField).mockReturnValue({ value: ['opt1'], @@ -187,7 +206,27 @@ describe('MultiselectField', () => { render(); const input = screen.getByTestId('multiselect-input'); - fireEvent.focus(input); + await user.click(input); expect(mockOnFocus).toHaveBeenCalled(); }); + + it('should call useMountEvent with element', () => { + const mockUseMountEvent = vi.mocked(useMountEvent); + render(); + expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should call useUnmountEvent with element', () => { + const mockUseUnmountEvent = vi.mocked(useUnmountEvent); + render(); + expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should render FieldErrors with element prop', () => { + render(); + expect(FieldErrors).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx index 67097dea31..b7a48f0215 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx @@ -1,6 +1,9 @@ import { PhoneNumberInput } from '@/components/atoms'; import { createTestId } from '@/components/organisms/Renderer'; +import { useCallback } from 'react'; import { useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormElement } from '../../types'; @@ -11,17 +14,27 @@ export interface IPhoneFieldParams { } export const PhoneField: TDynamicFormElement = ({ element }) => { + useMountEvent(element); + useUnmountEvent(element); + const { defaultCountry = 'us' } = element.params || {}; const { stack } = useStack(); const { value, onChange, onBlur, onFocus } = useField(element, stack); + const handleChange = useCallback( + (value: string) => { + onChange(value); + }, + [onChange], + ); + return ( diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx index a6269ad040..6e0eb28f9d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx @@ -4,6 +4,8 @@ import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { IFormElement } from '../../types'; @@ -18,6 +20,14 @@ vi.mock('../../hooks/external', () => ({ useField: vi.fn(), })); +vi.mock('../../hooks/internal/useMountEvent', () => ({ + useMountEvent: vi.fn(), +})); + +vi.mock('../../hooks/internal/useUnmountEvent', () => ({ + useUnmountEvent: vi.fn(), +})); + vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); @@ -40,7 +50,7 @@ describe('PhoneField', () => { params: {}, valueDestination: 'test.path', element: 'phonefield', - } as IFormElement; + } as unknown as IFormElement; const mockFieldValues = { value: '+1234567890', @@ -54,6 +64,8 @@ describe('PhoneField', () => { vi.mocked(useStack).mockReturnValue({ stack: [] }); vi.mocked(useField).mockReturnValue(mockFieldValues as any); vi.mocked(createTestId).mockReturnValue('test-id'); + vi.mocked(useMountEvent).mockReturnValue(undefined); + vi.mocked(useUnmountEvent).mockReturnValue(undefined); }); it('should render PhoneNumberInput with default country "us"', () => { @@ -64,7 +76,7 @@ describe('PhoneField', () => { country: 'us', testId: 'test-id', value: '+1234567890', - onChange: mockFieldValues.onChange, + onChange: expect.any(Function), onBlur: mockFieldValues.onBlur, onFocus: mockFieldValues.onFocus, }), @@ -118,4 +130,16 @@ describe('PhoneField', () => { expect(createTestId).toHaveBeenCalledWith(mockElement, mockStack); }); + + it('should call useMountEvent with element', () => { + const mockUseMountEvent = vi.mocked(useMountEvent); + render(); + expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should call useUnmountEvent with element', () => { + const mockUseUnmountEvent = vi.mocked(useUnmountEvent); + render(); + expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx index e4fffb29f8..8521a28548 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx @@ -1,6 +1,9 @@ import { DropdownInput } from '@/components/molecules'; import { createTestId } from '@/components/organisms/Renderer'; +import { useCallback } from 'react'; import { useElement, useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -17,6 +20,9 @@ export interface ISelectFieldParams { } export const SelectField: TDynamicFormField = ({ element }) => { + useMountEvent(element); + useUnmountEvent(element); + const { stack } = useStack(); const { id } = useElement(element, stack); const { value, disabled, onChange, onBlur, onFocus } = useField( @@ -26,6 +32,13 @@ export const SelectField: TDynamicFormField = ({ element }) const { placeholder, options = [] } = element.params || {}; + const handleChange = useCallback( + (value: string) => { + onChange(value); + }, + [onChange], + ); + return ( = ({ element }) placeholder: placeholder || '', }} disabled={disabled} - onChange={onChange} + onChange={handleChange} onBlur={onBlur} onFocus={onFocus} /> diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx index 15379556c1..bc36f4beae 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx @@ -1,8 +1,12 @@ import { DropdownInput } from '@/components/molecules'; import { createTestId } from '@/components/organisms/Renderer'; -import { fireEvent, render } from '@testing-library/react'; +import { cleanup, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useElement, useField } from '../../hooks/external'; +import { useEvents } from '../../hooks/internal/useEvents'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { TBaseFields } from '../../repositories/fields-repository'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; @@ -44,6 +48,26 @@ vi.mock('../FieldList/providers/StackProvider', () => ({ useStack: vi.fn(), })); +vi.mock('../../hooks/internal/useEvents', () => ({ + useEvents: vi.fn(), +})); + +vi.mock('../../hooks/internal/useMountEvent', () => ({ + useMountEvent: vi.fn(), +})); + +vi.mock('../../hooks/internal/useUnmountEvent', () => ({ + useUnmountEvent: vi.fn(), +})); + +vi.mock('../../layouts/FieldLayout', () => ({ + FieldLayout: ({ children }: any) =>
{children}
, +})); + +vi.mock('../../layouts/FieldErrors', () => ({ + FieldErrors: () => null, +})); + describe('SelectField', () => { const mockElement = { id: 'test-id', @@ -60,10 +84,15 @@ describe('SelectField', () => { const mockTestId = 'test-select-field'; beforeEach(() => { + cleanup(); vi.clearAllMocks(); vi.mocked(useStack).mockReturnValue({ stack: mockStack }); - vi.mocked(useElement).mockReturnValue({ id: mockElement.id } as ReturnType); + vi.mocked(useElement).mockReturnValue({ + id: mockElement.id, + originId: mockElement.id, + hidden: false, + } as ReturnType); vi.mocked(useField).mockReturnValue({ value: undefined, disabled: false, @@ -73,6 +102,10 @@ describe('SelectField', () => { touched: false, }); vi.mocked(createTestId).mockReturnValue(mockTestId); + vi.mocked(useEvents).mockReturnValue({ + sendEvent: vi.fn(), + sendEventAsync: vi.fn(), + } as unknown as ReturnType); }); it('should render DropdownInput with correct props', () => { @@ -128,7 +161,6 @@ describe('SelectField', () => { expect.objectContaining({ value: mockHandlers.value, disabled: mockHandlers.disabled, - onChange: mockHandlers.onChange, onBlur: mockHandlers.onBlur, onFocus: mockHandlers.onFocus, }), @@ -137,6 +169,7 @@ describe('SelectField', () => { }); it('should trigger onBlur when dropdown is closed', async () => { + const user = userEvent.setup(); const mockHandlers = { value: '1', disabled: false, @@ -151,12 +184,14 @@ describe('SelectField', () => { const { getByRole } = render(); const trigger = getByRole('combobox'); - fireEvent.blur(trigger); + await user.click(trigger); + await user.tab(); expect(mockHandlers.onBlur).toHaveBeenCalled(); }); it('should trigger onFocus when dropdown input is focused', async () => { + const user = userEvent.setup(); const mockHandlers = { value: '1', disabled: false, @@ -171,12 +206,13 @@ describe('SelectField', () => { const { getByRole } = render(); const trigger = getByRole('combobox'); - await trigger.focus(); + await user.click(trigger); expect(mockHandlers.onFocus).toHaveBeenCalled(); }); it('should render options when dropdown is opened', async () => { + const user = userEvent.setup(); const mockHandlers = { value: undefined, disabled: false, @@ -191,7 +227,7 @@ describe('SelectField', () => { const { getByRole, getByText } = render(); const trigger = getByRole('combobox'); - fireEvent.click(trigger); + await user.click(trigger); // Check that both options from mockElement are rendered expect(getByText('Option 1')).toBeInTheDocument(); @@ -199,6 +235,7 @@ describe('SelectField', () => { }); it('should call on change callback on value change', async () => { + const user = userEvent.setup(); const mockHandlers = { value: undefined, disabled: false, @@ -213,7 +250,7 @@ describe('SelectField', () => { const { getByRole } = render(); const trigger = getByRole('combobox'); - fireEvent.change(trigger, { target: { value: '1' } }); + await user.selectOptions(trigger, '1'); expect(mockHandlers.onChange).toHaveBeenCalledWith('1'); }); @@ -235,4 +272,28 @@ describe('SelectField', () => { const trigger = getByRole('combobox'); expect(trigger).toHaveTextContent('Option 2'); }); + + it('should call useMountEvent with element', () => { + const mockUseMountEvent = vi.mocked(useMountEvent); + render(); + expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should call useUnmountEvent with element', () => { + const mockUseUnmountEvent = vi.mocked(useUnmountEvent); + render(); + expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should trigger mount and unmount events in correct order', () => { + const mockUseMountEvent = vi.mocked(useMountEvent); + const mockUseUnmountEvent = vi.mocked(useUnmountEvent); + + const { unmount } = render(); + + expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); + expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); + + unmount(); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx index 799f6191f4..815e25cc41 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx @@ -3,6 +3,8 @@ import { Input } from '@/components/atoms/Input'; import { createTestId } from '@/components/organisms/Renderer'; import { useCallback } from 'react'; import { useElement, useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -16,15 +18,14 @@ export interface ITextFieldParams { } export const TextField: TDynamicFormField = ({ element }) => { + useMountEvent(element); + useUnmountEvent(element); + const { params } = element; const { valueType = 'string', style = 'text', placeholder } = params || {}; const { stack } = useStack(); - if (stack?.length) { - console.log('stack', stack, element); - } - const { id } = useElement(element, stack); const { value, onChange, onBlur, onFocus, disabled } = useField(element, stack); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx index 04f5d1ff04..d15d796b27 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx @@ -3,6 +3,9 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useElement, useField } from '../../hooks/external'; +import { useEvents } from '../../hooks/internal/useEvents'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { ITextFieldParams, TextField } from './TextField'; @@ -42,6 +45,26 @@ vi.mock('./helpers', () => ({ serializeTextFieldValue: vi.fn(), })); +vi.mock('../../hooks/internal/useEvents', () => ({ + useEvents: vi.fn(), +})); + +vi.mock('../../hooks/internal/useMount', () => ({ + useMount: vi.fn(), +})); + +vi.mock('../../hooks/internal/useUnmount', () => ({ + useUnmount: vi.fn(), +})); + +vi.mock('../../hooks/internal/useMountEvent', () => ({ + useMountEvent: vi.fn(), +})); + +vi.mock('../../hooks/internal/useUnmountEvent', () => ({ + useUnmountEvent: vi.fn(), +})); + describe('TextField', () => { const mockStack = [0]; const mockElement = { @@ -67,9 +90,17 @@ describe('TextField', () => { vi.clearAllMocks(); vi.mocked(useStack).mockReturnValue({ stack: mockStack }); vi.mocked(useField).mockReturnValue(mockFieldProps); - vi.mocked(useElement).mockReturnValue({ id: 'test-field' }); + vi.mocked(useElement).mockReturnValue({ + id: 'test-field', + originId: 'test-field', + hidden: false, + } as any); vi.mocked(createTestId).mockReturnValue('test-id'); vi.mocked(serializeTextFieldValue).mockImplementation(value => value); + vi.mocked(useEvents).mockReturnValue({ + sendEvent: vi.fn(), + sendEventAsync: vi.fn(), + } as unknown as ReturnType); }); it('should render Input component when style is text', () => { @@ -201,4 +232,28 @@ describe('TextField', () => { await user.tab(); expect(mockFieldProps.onBlur).toHaveBeenCalled(); }); + + it('should call useMountEvent with element', () => { + const mockUseMountEvent = vi.mocked(useMountEvent); + render(); + expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should call useUnmountEvent with element', () => { + const mockUseUnmountEvent = vi.mocked(useUnmountEvent); + render(); + expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); + }); + + it('should trigger mount and unmount events in correct order', () => { + const mockUseMountEvent = vi.mocked(useMountEvent); + const mockUseUnmountEvent = vi.mocked(useUnmountEvent); + + const { unmount } = render(); + + expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); + expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); + + unmount(); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts index 10704ae534..77fff37bc4 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.ts @@ -3,9 +3,6 @@ import { TDeepthLevelStack } from '@/components/organisms/Form/Validator'; import { useMemo } from 'react'; import { useDynamicForm } from '../../../context'; import { IFormElement } from '../../../types'; -import { useEvents } from '../../internal/useEvents'; -import { useMount } from '../../internal/useMount'; -import { useUnmount } from '../../internal/useUnmount'; import { useElementId } from '../useElementId'; import { useClearValueOnUnmount } from './hooks/useClearValueOnUnmount'; @@ -14,7 +11,6 @@ export const useElement = ( stack: TDeepthLevelStack = [], ) => { const { values } = useDynamicForm(); - const { sendEvent } = useEvents(element); const hiddenRulesResult = useRuleEngine(values, { rules: element.hidden, runOnInitialize: true, @@ -27,8 +23,6 @@ export const useElement = ( return hiddenRulesResult.every(result => result.result === true); }, [hiddenRulesResult]); - useMount(() => sendEvent('onMount')); - useUnmount(() => sendEvent('onUnmount')); useClearValueOnUnmount(element, isHidden); return { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts index b65f0c05f6..8076842d9e 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useElement/useElement.unit.test.ts @@ -7,8 +7,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useDynamicForm } from '../../../context'; import { IFormElement } from '../../../types'; import { useEvents } from '../../internal/useEvents'; -import { useMount } from '../../internal/useMount'; -import { useUnmount } from '../../internal/useUnmount'; import { useElementId } from '../useElementId'; import { useClearValueOnUnmount } from './hooks/useClearValueOnUnmount'; import { useElement } from './useElement'; @@ -16,8 +14,6 @@ import { useElement } from './useElement'; vi.mock('@/components/organisms/Form/hooks/useRuleEngine'); vi.mock('../../../context'); vi.mock('../../internal/useEvents'); -vi.mock('../../internal/useMount'); -vi.mock('../../internal/useUnmount'); vi.mock('../useElementId'); vi.mock('./hooks/useClearValueOnUnmount'); @@ -125,36 +121,6 @@ describe('useElement', () => { }); describe('lifecycle events', () => { - it('should call sendEvent with onMount on mount', () => { - const element = { id: 'test-id' } as IFormElement; - - renderHook(() => useElement(element)); - - expect(useMount).toHaveBeenCalledWith(expect.any(Function)); - const mountCallback = vi.mocked(useMount).mock.calls[0]?.[0]; - - if (mountCallback) { - mountCallback(); - } - - expect(mockSendEvent).toHaveBeenCalledWith('onMount'); - }); - - it('should call sendEvent with onUnmount on unmount', () => { - const element = { id: 'test-id' } as IFormElement; - - renderHook(() => useElement(element)); - - expect(useUnmount).toHaveBeenCalledWith(expect.any(Function)); - const unmountCallback = vi.mocked(useUnmount).mock.calls[0]?.[0]; - - if (unmountCallback) { - unmountCallback(); - } - - expect(mockSendEvent).toHaveBeenCalledWith('onUnmount'); - }); - it('should call useClearValueOnUnmount with element and hidden state', () => { const element = { id: 'test-id' } as IFormElement; vi.mocked(useRuleEngine).mockReturnValue([ diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/index.ts new file mode 100644 index 0000000000..94ad2e0aa9 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/index.ts @@ -0,0 +1 @@ +export * from './useMountEvent'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/useMountEvent.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/useMountEvent.ts new file mode 100644 index 0000000000..0b8ae28595 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/useMountEvent.ts @@ -0,0 +1,9 @@ +import { IFormElement } from '../../../types'; +import { useEvents } from '../useEvents'; +import { useMount } from '../useMount'; + +export const useMountEvent = (element: IFormElement) => { + const { sendEvent } = useEvents(element); + + useMount(() => sendEvent('onMount')); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/useMountEvent.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/useMountEvent.unit.test.ts new file mode 100644 index 0000000000..9a4cef7018 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useMountEvent/useMountEvent.unit.test.ts @@ -0,0 +1,43 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { IFormElement } from '../../../types'; +import { useEvents } from '../useEvents'; +import { useMount } from '../useMount'; +import { useMountEvent } from './useMountEvent'; + +vi.mock('../useEvents'); +vi.mock('../useMount'); + +describe('useMountEvent', () => { + const mockSendEvent = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useEvents).mockReturnValue({ sendEvent: mockSendEvent } as any); + vi.mocked(useMount).mockImplementation(callback => callback()); + }); + + it('should call useEvents with provided element', () => { + const element = { id: 'test-id' } as IFormElement; + + renderHook(() => useMountEvent(element)); + + expect(useEvents).toHaveBeenCalledWith(element); + }); + + it('should call sendEvent with onMount when mounted', () => { + const element = { id: 'test-id' } as IFormElement; + + renderHook(() => useMountEvent(element)); + + expect(mockSendEvent).toHaveBeenCalledWith('onMount'); + }); + + it('should call useMount with callback function', () => { + const element = { id: 'test-id' } as IFormElement; + + renderHook(() => useMountEvent(element)); + + expect(useMount).toHaveBeenCalledWith(expect.any(Function)); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/index.ts new file mode 100644 index 0000000000..d5abc6b706 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/index.ts @@ -0,0 +1 @@ +export * from './useUnmountEvent'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/useUnmountEvent.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/useUnmountEvent.ts new file mode 100644 index 0000000000..d2aca2d646 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/useUnmountEvent.ts @@ -0,0 +1,9 @@ +import { IFormElement } from '../../../types'; +import { useEvents } from '../useEvents'; +import { useUnmount } from '../useUnmount'; + +export const useUnmountEvent = (element: IFormElement) => { + const { sendEvent } = useEvents(element); + + useUnmount(() => sendEvent('onUnmount')); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/useUnmountEvent.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/useUnmountEvent.unit.test.ts new file mode 100644 index 0000000000..7602c94086 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useUnmountEvent/useUnmountEvent.unit.test.ts @@ -0,0 +1,43 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { IFormElement } from '../../../types'; +import { useEvents } from '../useEvents'; +import { useUnmount } from '../useUnmount'; +import { useUnmountEvent } from './useUnmountEvent'; + +vi.mock('../useEvents'); +vi.mock('../useUnmount'); + +describe('useUnmountEvent', () => { + const mockSendEvent = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useEvents).mockReturnValue({ sendEvent: mockSendEvent } as any); + vi.mocked(useUnmount).mockImplementation(callback => callback()); + }); + + it('should call useEvents with provided element', () => { + const element = { id: 'test-id' } as IFormElement; + + renderHook(() => useUnmountEvent(element)); + + expect(useEvents).toHaveBeenCalledWith(element); + }); + + it('should call sendEvent with onUnmount when unmounted', () => { + const element = { id: 'test-id' } as IFormElement; + + renderHook(() => useUnmountEvent(element)); + + expect(mockSendEvent).toHaveBeenCalledWith('onUnmount'); + }); + + it('should call useUnmount with callback function', () => { + const element = { id: 'test-id' } as IFormElement; + + renderHook(() => useUnmountEvent(element)); + + expect(useUnmount).toHaveBeenCalledWith(expect.any(Function)); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.ts b/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.ts index d7f265866c..abef8da76d 100644 --- a/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.ts +++ b/packages/ui/src/components/organisms/Form/Validator/utils/format-value-destination/format-value-destination.ts @@ -4,7 +4,7 @@ export const formatValueDestination = (valueDestination: string, stack: TDeepthL let _valueDestination = valueDestination; stack.forEach((stack, index) => { - _valueDestination = _valueDestination.replace(`$${index}`, stack.toString()); + _valueDestination = _valueDestination?.replace(`$${index}`, stack.toString()); }); return _valueDestination; diff --git a/packages/ui/src/main.tsx b/packages/ui/src/main.tsx new file mode 100644 index 0000000000..4d934ff4f3 --- /dev/null +++ b/packages/ui/src/main.tsx @@ -0,0 +1,9 @@ +import ReactDOM from 'react-dom/client'; +import { InputsShowcaseComponent } from './components/organisms/Form/DynamicForm/_stories/InputsShowcase'; +import './global.css'; + +const App = () => { + return ; +}; + +ReactDOM.createRoot(document.getElementById('root')!).render(); From 7ebe91b8beea5f8261493e09cf876a2d9dbcaa5a Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Thu, 19 Dec 2024 15:31:07 +0200 Subject: [PATCH 25/54] feat: added configurable file upload --- packages/ui/package.json | 1 + .../Form/DynamicForm/DynamicForm.stories.tsx | 7 +- .../Form/DynamicForm/DynamicForm.tsx | 4 +- .../DynamicForm/DynamicForm.unit.test.tsx | 3 + .../FileUploadShowcase/FileUploadShowcase.tsx | 70 ++ .../_stories/FileUploadShowcase/index.ts | 1 + .../InputsShowcase/InputsShowcase.tsx | 2 +- .../Form/DynamicForm/context/types.ts | 1 + .../fields/FileField/FileField.tsx | 25 +- .../FileField/hooks/useFileUpload/helpers.ts | 49 ++ .../hooks/useFileUpload/helpers.unit.test.ts | 127 +++ .../FileField/hooks/useFileUpload/index.ts | 1 + .../hooks/useFileUpload/useFileUpload.ts | 53 ++ .../organisms/Form/DynamicForm/types/index.ts | 1 + pnpm-lock.yaml | 750 ++++++++++++------ 15 files changed, 825 insertions(+), 270 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index 896745ac0b..6ca8b62b3c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -50,6 +50,7 @@ "ajv": "^8.12.0", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", + "axios": "^1.7.9", "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", "cmdk": "^0.2.0", diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx index e9453c8dfc..27269ee1bc 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx @@ -1,5 +1,6 @@ import { DynamicFormV2 } from './DynamicForm'; -import { InputsShowcaseComponent } from './_stories/InputsShowcase/InputsShowcase'; +import { FileUploadShowcaseComponent } from './_stories/FileUploadShowcase'; +import { InputsShowcaseComponent } from './_stories/InputsShowcase'; export default { component: DynamicFormV2, @@ -8,3 +9,7 @@ export default { export const InputsShowcase = { render: () => , }; + +export const FileUploadShowcase = { + render: () => , +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx index 8335f563b8..ba94c732a1 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -16,6 +16,7 @@ export const DynamicFormV2: FunctionComponent = ({ values: initialValues, validationParams, fieldExtends, + metadata, onChange, onFieldChange, onSubmit, @@ -41,8 +42,9 @@ export const DynamicFormV2: FunctionComponent = ({ callbacks: { onEvent, }, + metadata, }), - [touchedApi.touched, valuesApi.values, submit, fieldHelpers, fieldExtends, onEvent], + [touchedApi.touched, valuesApi.values, submit, fieldHelpers, fieldExtends, onEvent, metadata], ); return ( 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 cd9b80d148..27053163ae 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 @@ -79,6 +79,7 @@ describe('DynamicFormV2', () => { onFieldChange: vi.fn(), onSubmit: vi.fn(), onEvent: vi.fn(), + metadata: {}, } as unknown as IDynamicFormProps; it('should render without crashing', () => { @@ -145,6 +146,7 @@ describe('DynamicFormV2', () => { getValue: vi.fn(), setTouched: vi.fn(), setValue: vi.fn(), + touchAllFields: vi.fn(), }; vi.mocked(useTouched).mockReturnValue(touchedMock); @@ -166,6 +168,7 @@ describe('DynamicFormV2', () => { callbacks: { onEvent: mockProps.onEvent, }, + metadata: mockProps.metadata, }); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx new file mode 100644 index 0000000000..d84849f57f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx @@ -0,0 +1,70 @@ +import { AnyObject } from '@/common'; +import { useState } from 'react'; +import { JSONEditorComponent } from '../../../Validator/_stories/components/JsonEditor/JsonEditor'; +import { DynamicFormV2 } from '../../DynamicForm'; +import { IFormElement } from '../../types'; + +const schema: Array> = [ + { + id: 'FileField:Regular', + element: 'filefield', + valueDestination: 'file-regular', + params: { + label: 'Regular Upload', + placeholder: 'Select File', + uploadSettings: { + url: 'http://localhost:3000/upload', + resultPath: 'filename', + }, + }, + }, + { + id: 'FileField:Protected', + element: 'filefield', + valueDestination: 'file-protected', + params: { + label: 'Upload on Submit', + placeholder: 'Select File', + uploadSettings: { + url: 'http://localhost:3000/upload-protected', + resultPath: 'filename', + headers: { + Authorization: '{token}', + }, + }, + }, + }, + { + id: 'SubmitButton', + element: 'submitbutton', + valueDestination: 'submitbutton', + params: { + label: 'Submit Button', + }, + }, +]; + +export const FileUploadShowcaseComponent = () => { + const [context, setContext] = useState({}); + + return ( +
+
+ { + console.log('onSubmit'); + }} + onChange={setContext} + metadata={{ + token: '1234', + }} + /> +
+
+ +
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/index.ts new file mode 100644 index 0000000000..fb5f0fcfdb --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/index.ts @@ -0,0 +1 @@ +export * from './FileUploadShowcase'; 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 80c5ef24c7..a118477ad7 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 @@ -151,7 +151,7 @@ export const InputsShowcaseComponent = () => { console.log('onSubmit'); }} onChange={setContext} - onEvent={console.log} + // onEvent={console.log} />
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 e34611e7b0..af488d780e 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts @@ -14,4 +14,5 @@ export interface IDynamicFormContext { fieldHelpers: IFieldHelpers; submit: () => void; callbacks: IDynamicFormCallbacks; + metadata: Record; } diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx index ab4f8ec53a..3610b3f916 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx @@ -4,20 +4,22 @@ import { Input } from '@/components/atoms/Input'; import { createTestId } from '@/components/organisms/Renderer'; import { Upload, XCircle } from 'lucide-react'; import { useCallback, useMemo, useRef } from 'react'; -import { useElement, useField } from '../../hooks/external'; +import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { ICommonFieldParams, TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; +import { useFileUpload } from './hooks/useFileUpload'; export interface IFileFieldParams extends ICommonFieldParams { uploadOn?: 'change' | 'submit'; uploadSettings?: { url: string; - method: 'POST' | 'PUT'; - headers: Record; + resultPath: string; + headers?: Record; + method?: 'POST' | 'PUT'; }; acceptFileFormats?: string; } @@ -26,15 +28,10 @@ export const FileField: TDynamicFormField = ({ element }) => { useMountEvent(element); useUnmountEvent(element); - const { - uploadOn = 'change', - uploadSettings = {}, - placeholder = 'Choose file', - acceptFileFormats = undefined, - } = element.params || {}; + const { placeholder = 'Choose file', acceptFileFormats = undefined } = element.params || {}; + const { handleChange } = useFileUpload(element, element.params); const { stack } = useStack(); - const { id } = useElement(element, stack); const { value, disabled, onChange, onBlur, onFocus } = useField( element, stack, @@ -89,13 +86,7 @@ export const FileField: TDynamicFormField = ({ element }) => { placeholder={placeholder} accept={acceptFileFormats} disabled={disabled} - onChange={e => { - const file = e.target.files?.[0]; - - if (file) { - onChange(file); - } - }} + onChange={handleChange} onBlur={onBlur} onFocus={onFocus} ref={inputRef} diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.ts new file mode 100644 index 0000000000..48c89841e9 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.ts @@ -0,0 +1,49 @@ +import axios from 'axios'; +import get from 'lodash/get'; +import { IFileFieldParams } from '../../FileField'; + +export const formatHeaders = ( + headers: Record, + metadata: Record = {}, +) => { + const formattedHeaders: Record = {}; + + Object.entries(headers).forEach(([key, value]) => { + let formattedValue = value; + const matches = value.match(/\{([^}]+)\}/g); + + if (matches) { + matches.forEach(match => { + const metadataKey = match.slice(1, -1); + + if (metadata[metadataKey]) { + formattedValue = formattedValue.replace(match, metadata[metadataKey]); + } + }); + } + + formattedHeaders[key] = formattedValue; + }); + + return formattedHeaders; +}; + +export const uploadFile = async (file: File, params: IFileFieldParams['uploadSettings']) => { + if (!params) { + throw new Error('Upload settings are required to upload a file'); + } + + const { url, method = 'POST', headers = {} } = params; + + const formData = new FormData(); + formData.append('file', file); + + const response = await axios({ + method, + url, + headers, + data: formData, + }); + + return get(response.data, params.resultPath); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.unit.test.ts new file mode 100644 index 0000000000..e2c234067d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.unit.test.ts @@ -0,0 +1,127 @@ +import axios from 'axios'; +import { describe, expect, it, vi } from 'vitest'; +import { formatHeaders, uploadFile } from './helpers'; + +vi.mock('axios'); +const mockedAxios = vi.mocked(axios); + +describe('formatHeaders', () => { + it('should return empty object when no headers provided', () => { + const result = formatHeaders({}); + expect(result).toEqual({}); + }); + + it('should return headers without modification when no metadata matches', () => { + const headers = { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + }; + const result = formatHeaders(headers); + expect(result).toEqual(headers); + }); + + it('should replace metadata placeholders in headers', () => { + const headers = { + Authorization: 'Bearer {token}', + 'X-User-Id': '{userId}', + }; + const metadata = { + token: 'abc123', + userId: '12345', + }; + const expected = { + Authorization: 'Bearer abc123', + 'X-User-Id': '12345', + }; + const result = formatHeaders(headers, metadata); + expect(result).toEqual(expected); + }); + + it('should keep original placeholder if metadata key not found', () => { + const headers = { + Authorization: 'Bearer {token}', + 'X-User-Id': '{userId}', + }; + const metadata = { + token: 'abc123', + }; + const expected = { + Authorization: 'Bearer abc123', + 'X-User-Id': '{userId}', + }; + const result = formatHeaders(headers, metadata); + expect(result).toEqual(expected); + }); +}); + +describe('uploadFile', () => { + const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' }); + const mockParams = { + url: 'http://test.com/upload', + method: 'POST' as const, + headers: { 'Content-Type': 'multipart/form-data' }, + resultPath: 'fileUrl', + }; + + it('should throw error if no params provided', async () => { + await expect(uploadFile(mockFile, undefined)).rejects.toThrow( + 'Upload settings are required to upload a file', + ); + }); + + it('should upload file successfully and return result from specified path', async () => { + const mockResponse = { + data: { + fileUrl: 'http://test.com/files/test.txt', + }, + }; + + mockedAxios.mockResolvedValueOnce(mockResponse); + + const result = await uploadFile(mockFile, mockParams); + + expect(mockedAxios).toHaveBeenCalledWith({ + method: 'POST', + url: mockParams.url, + headers: mockParams.headers, + data: expect.any(FormData), + }); + expect(result).toBe(mockResponse.data.fileUrl); + }); + + it('should use POST as default method if not specified', async () => { + const paramsWithoutMethod = { + url: 'http://test.com/upload', + headers: {}, + resultPath: 'data.fileUrl', + }; + + mockedAxios.mockResolvedValueOnce({ data: { fileUrl: 'test' } }); + + await uploadFile(mockFile, paramsWithoutMethod); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + }), + ); + }); + + it('should use empty object as default headers if not specified', async () => { + const paramsWithoutHeaders = { + url: 'http://test.com/upload', + method: 'POST' as const, + resultPath: 'data.fileUrl', + }; + + mockedAxios.mockResolvedValueOnce({ data: { fileUrl: 'test' } }); + + await uploadFile(mockFile, paramsWithoutHeaders); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + headers: {}, + }), + ); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/index.ts new file mode 100644 index 0000000000..54e4a46719 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/index.ts @@ -0,0 +1 @@ +export * from './useFileUpload'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts new file mode 100644 index 0000000000..e0446de3a7 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts @@ -0,0 +1,53 @@ +import { useCallback, useState } from 'react'; +import { useDynamicForm } from '../../../../context'; +import { useField } from '../../../../hooks/external'; +import { IFormElement } from '../../../../types'; +import { IFileFieldParams } from '../../FileField'; +import { formatHeaders, uploadFile } from './helpers'; + +export const useFileUpload = ( + element: IFormElement, + params: IFileFieldParams = {}, +) => { + const { uploadOn = 'change' } = params; + const [isUploading, setIsUploading] = useState(false); + const { metadata } = useDynamicForm(); + + const { onChange } = useField(element); + + const handleChange = useCallback( + async (e: React.ChangeEvent) => { + const { uploadSettings } = params; + + const uploadParams = { + ...uploadSettings, + method: uploadSettings?.method || 'POST', + headers: formatHeaders(uploadSettings?.headers || {}, metadata), + }; + + console.log('uploadParams', uploadParams); + + if (uploadOn === 'change') { + try { + setIsUploading(true); + + const result = await uploadFile( + e.target?.files?.[0] as File, + uploadParams as IFileFieldParams['uploadSettings'], + ); + onChange(result); + } catch (error) { + console.error('Failed to upload file.', error); + } finally { + setIsUploading(false); + } + } + }, + [uploadOn, params, metadata, onChange], + ); + + return { + isUploading, + handleChange, + }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts index fac8d1498f..7ad64143f1 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts @@ -55,4 +55,5 @@ export interface IDynamicFormProps { onEvent?: (eventName: TElementEvent, element: IFormEventElement) => void; ref?: React.RefObject>; + metadata?: Record; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce45d7a995..89cc199d68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1799,6 +1799,9 @@ importers: ajv-formats: specifier: ^2.1.1 version: 2.1.1(ajv@8.13.0) + axios: + specifier: ^1.7.9 + version: 1.7.9 class-variance-authority: specifier: ^0.6.1 version: 0.6.1 @@ -4844,7 +4847,7 @@ packages: '@babel/traverse': 7.25.6 '@babel/types': 7.25.6 convert-source-map: 2.0.0 - debug: 4.3.6 + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4954,7 +4957,25 @@ packages: '@babel/helper-function-name': 7.23.0 '@babel/helper-member-expression-to-functions': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: true + + /@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.25.2): + resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 semver: 6.3.1 @@ -4972,13 +4993,13 @@ packages: semver: 6.3.1 dev: true - /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.23.7): + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.25.2): resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.22.5 regexpu-core: 5.3.2 semver: 6.3.1 @@ -5000,12 +5021,12 @@ packages: - supports-color dev: true - /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.23.7): + /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.25.2): resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 debug: 4.3.6 @@ -5095,6 +5116,20 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true + /@babel/helper-module-transforms@7.23.3(@babel/core@7.25.2): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + /@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2): resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} engines: {node: '>=6.9.0'} @@ -5133,13 +5168,13 @@ packages: '@babel/helper-wrap-function': 7.22.20 dev: true - /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.7): + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.25.2): resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-wrap-function': 7.22.20 @@ -5157,13 +5192,13 @@ packages: '@babel/helper-optimise-call-expression': 7.22.5 dev: true - /@babel/helper-replace-supers@7.22.20(@babel/core@7.23.7): + /@babel/helper-replace-supers@7.22.20(@babel/core@7.25.2): resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-member-expression-to-functions': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 @@ -5326,13 +5361,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.23.7): + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5348,25 +5383,25 @@ packages: '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.17.9) dev: true - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.23.7): + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.25.2) dev: true - /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.23.7): + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5571,13 +5606,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.7): + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2): resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 dev: true /@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.17.9): @@ -5624,6 +5659,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.2): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: @@ -5651,6 +5695,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.2): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.17.9): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} @@ -5661,13 +5714,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.7): + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.2): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5680,12 +5733,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.25.2): resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5698,12 +5751,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.25.2): resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5717,23 +5770,23 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.23.7): + /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5746,6 +5799,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.2): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: @@ -5764,6 +5826,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.2): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.17.9): resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} engines: {node: '>=6.9.0'} @@ -5812,6 +5883,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.2): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: @@ -5830,6 +5910,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.2): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.17.9): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: @@ -5848,6 +5937,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.2): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: @@ -5866,6 +5964,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.2): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: @@ -5884,6 +5991,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.2): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.17.9): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: @@ -5902,6 +6018,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.2): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.17.9): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} @@ -5912,13 +6037,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.7): + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.2): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5942,6 +6067,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.2): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.17.9): resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} engines: {node: '>=6.9.0'} @@ -5962,14 +6097,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.7): + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.25.2): resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -5983,27 +6118,27 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-async-generator-functions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-async-generator-functions@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-59GsVNavGxAXCDDbakWSMJhajASb4kBCqDjqJsv+p5nKdbz7istmZ3HrX3L2LuiI80+zsOADCvooqQH3qGCucQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.7) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.7) + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) dev: true /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.17.9): @@ -6018,16 +6153,16 @@ packages: '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.17.9) dev: true - /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-module-imports': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.7) + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.25.2) dev: true /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.17.9): @@ -6040,13 +6175,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6060,37 +6195,37 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-block-scoping@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-block-scoping@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-QPZxHrThbQia7UdvfpaRRlq/J9ciz1J4go0k+lPBXbgaNeY7IQrBj/9ceWjvMMI07/ZBzHl/F0R/2K0qH7jCVw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-class-static-block@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-class-static-block@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-PENDVxdr7ZxKPyi5Ffc0LjXdnJyrJxyqF5T5YjlVg4a0VFfQHW0r8iAtRiDXkfHlu1wwcvdtnndGYIeJLSuRMQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) dev: true /@babel/plugin-transform-classes@7.23.3(@babel/core@7.17.9): @@ -6111,20 +6246,20 @@ packages: globals: 11.12.0 dev: true - /@babel/plugin-transform-classes@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-classes@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) '@babel/helper-split-export-declaration': 7.22.6 globals: 11.12.0 dev: true @@ -6140,13 +6275,13 @@ packages: '@babel/template': 7.22.15 dev: true - /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/template': 7.22.15 dev: true @@ -6161,13 +6296,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6182,14 +6317,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6203,25 +6338,25 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-dynamic-import@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-dynamic-import@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-vTG+cTGxPFou12Rj7ll+eD5yWeNl5/8xvQvF08y5Gv3v4mZQoyFf8/n9zg4q5vvCWt5jmgymfzMAldO7orBn7A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.17.9): @@ -6235,26 +6370,26 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-export-namespace-from@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-export-namespace-from@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-yCLhW34wpJWRdTxxWtFZASJisihrfyMOTOQexhVzA78jlU+dH7Dw+zQgcPepQ5F3C6bAIiblZZ+qBggJdHiBAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.23.7): @@ -6278,13 +6413,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-for-of@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-for-of@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6300,27 +6435,27 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-function-name': 7.23.0 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-json-strings@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-json-strings@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-H9Ej2OiISIZowZHaBwF0tsJOih1PftXJtE8EWqlEIwpc7LMTGq0rPOrywKLQ4nefzx8/HMR0D3JGXoMHYvhi0A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-literals@7.23.3(@babel/core@7.17.9): @@ -6333,25 +6468,25 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-literals@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-logical-assignment-operators@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-logical-assignment-operators@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-+pD5ZbxofyOygEp+zZAfujY2ShNCXRpDRIPOiBmTO693hhyOEteZgl876Xs9SAHPQpcV0vz8LvA/T+w8AzyX8A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) dev: true /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.17.9): @@ -6364,13 +6499,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6385,14 +6520,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6415,7 +6550,19 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.23.7 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.25.2): + resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-simple-access': 7.22.5 dev: true @@ -6433,15 +6580,15 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true - /@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-identifier': 7.22.20 dev: true @@ -6457,14 +6604,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6479,14 +6626,14 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.23.7): + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.25.2): resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6500,50 +6647,50 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-nullish-coalescing-operator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-nullish-coalescing-operator@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-xzg24Lnld4DYIdysyf07zJ1P+iIfJpxtVFOzX4g+bsJ3Ng5Le7rXx9KwqKzuyaUeRnt+I1EICwQITqc0E2PmpA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) dev: true - /@babel/plugin-transform-numeric-separator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-numeric-separator@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-s9GO7fIBi/BLsZ0v3Rftr6Oe4t0ctJ8h4CCXfPoEJwmvAPMyNrfkOOJzm6b9PX9YXcCJWWQd/sBF/N26eBiMVw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) dev: true - /@babel/plugin-transform-object-rest-spread@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-object-rest-spread@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-VxHt0ANkDmu8TANdE9Kc0rndo/ccsmfe2Cx2y5sI4hu3AukHQ5wAu4cM7j3ba8B9548ijVyclBU+nuDQftZsog==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/compat-data': 7.23.5 - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.17.9): @@ -6557,26 +6704,26 @@ packages: '@babel/helper-replace-supers': 7.22.20(@babel/core@7.17.9) dev: true - /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.25.2) dev: true - /@babel/plugin-transform-optional-catch-binding@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-optional-catch-binding@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-LxYSb0iLjUamfm7f1D7GpiS4j0UAC8AOiehnsGAP8BEsIX8EOi3qV6bbctw8M7ZvLtcoZfZX5Z7rN9PlWk0m5A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-optional-chaining@7.23.3(@babel/core@7.17.9): @@ -6591,16 +6738,16 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.17.9) dev: true - /@babel/plugin-transform-optional-chaining@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-optional-chaining@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-zvL8vIfIUgMccIAK1lxjvNv572JHFJIKb4MWBz5OGdBQA0fB0Xluix5rmOby48exiJc987neOmP/m9Fnpkz3Tg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) dev: true /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.17.9): @@ -6613,38 +6760,38 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-private-property-in-object@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-private-property-in-object@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-a5m2oLNFyje2e/rGKjVfAELTVI5mbA0FeZpBnkOWWV7eSmKQ+T/XW0Vf+29ScLzSxX+rnsarvU0oie/4m6hkxA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) dev: true /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.17.9): @@ -6657,13 +6804,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6777,13 +6924,13 @@ packages: regenerator-transform: 0.15.2 dev: true - /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 regenerator-transform: 0.15.2 dev: true @@ -6798,13 +6945,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6818,13 +6965,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6839,13 +6986,13 @@ packages: '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true - /@babel/plugin-transform-spread@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-spread@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true @@ -6860,13 +7007,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6880,13 +7027,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6900,13 +7047,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6946,24 +7093,24 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -6978,25 +7125,25 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.25.2): resolution: {integrity: sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -7096,80 +7243,171 @@ packages: '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.7) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.7) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.7) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.7) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.7) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.7) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.7) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.7) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.7) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.23.7) - '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-async-generator-functions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-block-scoping': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-class-static-block': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-classes': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-dynamic-import': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-export-namespace-from': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-json-strings': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-logical-assignment-operators': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.23.7) - '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-numeric-separator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-object-rest-spread': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-optional-catch-binding': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-private-property-in-object': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.23.7) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.23.7) - babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.23.7) - babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.23.7) - babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.23.7) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.2) + '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-async-generator-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoping': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-class-static-block': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-classes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-dynamic-import': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-export-namespace-from': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-json-strings': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-logical-assignment-operators': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-nullish-coalescing-operator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-numeric-separator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-object-rest-spread': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-optional-catch-binding': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-private-property-in-object': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.25.2) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.2) + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.25.2) + babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.25.2) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.25.2) + core-js-compat: 3.33.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-env@7.23.3(@babel/core@7.25.2): + resolution: {integrity: sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.23.5 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.2) + '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-async-generator-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoping': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-class-static-block': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-classes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-dynamic-import': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-export-namespace-from': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-json-strings': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-logical-assignment-operators': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-nullish-coalescing-operator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-numeric-separator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-object-rest-spread': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-optional-catch-binding': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-optional-chaining': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-private-property-in-object': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.25.2) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.2) + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.25.2) + babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.25.2) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.25.2) core-js-compat: 3.33.2 semver: 6.3.1 transitivePeerDependencies: @@ -7201,12 +7439,12 @@ packages: esutils: 2.0.3 dev: true - /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.23.7): + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.25.2): resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/types': 7.23.6 esutils: 2.0.3 @@ -9696,7 +9934,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.6 + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -16243,7 +16481,7 @@ packages: hasBin: true dependencies: '@babel/core': 7.23.7 - '@babel/preset-env': 7.23.3(@babel/core@7.23.7) + '@babel/preset-env': 7.23.3(@babel/core@7.25.2) '@babel/types': 7.23.6 '@ndelangen/get-tarball': 3.0.9 '@storybook/codemod': 7.5.3 @@ -18205,7 +18443,7 @@ packages: resolution: {integrity: sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==} deprecated: This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed! dependencies: - axios: 1.6.2 + axios: 1.6.8(debug@4.3.4) transitivePeerDependencies: - debug dev: true @@ -21795,6 +22033,7 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug + dev: false /axios@1.6.8(debug@4.3.4): resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} @@ -21805,6 +22044,16 @@ packages: transitivePeerDependencies: - debug + /axios@1.7.9: + resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} + dependencies: + follow-redirects: 1.15.6(debug@4.3.4) + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /b4a@1.6.4: resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} dev: false @@ -21880,14 +22129,14 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.23.7): + /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.25.2): resolution: {integrity: sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: '@babel/compat-data': 7.23.5 - '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -21905,13 +22154,13 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.23.7): + /babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.25.2): resolution: {integrity: sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) core-js-compat: 3.33.2 transitivePeerDependencies: - supports-color @@ -21928,13 +22177,13 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.23.7): + /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.25.2): resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.7) + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) transitivePeerDependencies: - supports-color dev: true @@ -24056,7 +24305,6 @@ packages: optional: true dependencies: ms: 2.1.3 - dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -24834,6 +25082,7 @@ packages: /es-module-lexer@1.4.1: resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} + dev: false /es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} @@ -25963,7 +26212,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.22.0)(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.22.0)(typescript@5.5.4) eslint: 8.22.0 eslint-rule-composer: 0.3.0 dev: true @@ -26267,7 +26516,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.6 + debug: 4.4.0 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -26954,6 +27203,7 @@ packages: peerDependenciesMeta: debug: optional: true + dev: false /follow-redirects@1.15.6(debug@4.3.4): resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} @@ -39358,7 +39608,7 @@ packages: browserslist: 4.23.3 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 - es-module-lexer: 1.4.1 + es-module-lexer: 1.5.4 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 From 93eec1ef18bf0cac00872426325bcf2c969781c9 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Thu, 19 Dec 2024 15:55:23 +0200 Subject: [PATCH 26/54] feat: added task runner & tests --- .../Form/DynamicForm/DynamicForm.tsx | 18 ++- .../DynamicForm/DynamicForm.unit.test.tsx | 11 ++ .../providers/TaskRunner/TaskRunner.tsx | 41 +++++ .../TaskRunner/TaskRunner.unit.test.tsx | 147 ++++++++++++++++++ .../providers/TaskRunner/context/index.ts | 4 + .../TaskRunner/hooks/useTaskRunner/index.ts | 1 + .../hooks/useTaskRunner/useTaskRunner.ts | 4 + .../useTaskRunner/useTaskRunner.unit.test.tsx | 30 ++++ .../DynamicForm/providers/TaskRunner/index.ts | 1 + .../providers/TaskRunner/types/index.ts | 15 ++ 10 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/TaskRunner.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/TaskRunner.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/context/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/useTaskRunner.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/useTaskRunner.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/types/index.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx index ba94c732a1..bb22dad93c 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -8,6 +8,7 @@ import { useFieldHelpers } from './hooks/internal/useFieldHelpers'; import { useTouched } from './hooks/internal/useTouched'; import { useValidationSchema } from './hooks/internal/useValidationSchema'; import { useValues } from './hooks/internal/useValues'; +import { TaskRunner } from './providers/TaskRunner'; import { extendFieldsRepository, getFieldsRepository } from './repositories'; import { IDynamicFormProps } from './types'; @@ -42,16 +43,21 @@ export const DynamicFormV2: FunctionComponent = ({ callbacks: { onEvent, }, - metadata, + metadata: metadata ?? {}, }), [touchedApi.touched, valuesApi.values, submit, fieldHelpers, fieldExtends, onEvent, metadata], ); return ( - - - - - + + + + + + + ); }; 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 27053163ae..6ebd5cc7b8 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 @@ -9,6 +9,7 @@ import { useFieldHelpers } from './hooks/internal/useFieldHelpers'; import { useTouched } from './hooks/internal/useTouched'; import { useValidationSchema } from './hooks/internal/useValidationSchema'; import { useValues } from './hooks/internal/useValues'; +import { TaskRunner } from './providers/TaskRunner'; import { ICommonFieldParams, IDynamicFormProps, IFormElement } from './types'; // Mock dependencies @@ -26,6 +27,8 @@ vi.mock('./hooks/internal/useValidationSchema'); vi.mock('./hooks/internal/useValues'); +vi.mock('./providers/TaskRunner'); + vi.mock('./context', () => ({ DynamicFormContext: { Provider: vi.fn(({ children, value }: any) => { @@ -45,6 +48,9 @@ describe('DynamicFormV2', () => { vi.mocked(ValidatorProvider).mockImplementation(({ children }: any) => { return
{children}
; }); + vi.mocked(TaskRunner).mockImplementation(({ children }: any) => { + return
{children}
; + }); vi.mocked(useTouched).mockReturnValue({ touched: {}, @@ -86,6 +92,11 @@ describe('DynamicFormV2', () => { render(); }); + it('should render TaskRunner component', () => { + const { getByTestId } = render(); + expect(getByTestId('task-runner')).toBeInTheDocument(); + }); + it('should pass elements to useValidationSchema', () => { const elements = [{ id: 'test', element: 'textfield' }] as unknown as Array< IFormElement diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/TaskRunner.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/TaskRunner.tsx new file mode 100644 index 0000000000..b36762341f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/TaskRunner.tsx @@ -0,0 +1,41 @@ +import { ReactNode, useCallback, useMemo, useState } from 'react'; +import { TaskRunnerContext } from './context'; +import { ITask } from './types'; + +interface ITaskRunnerProps { + children: ReactNode; +} + +export const TaskRunner = ({ children }: ITaskRunnerProps) => { + const [tasks, setTasks] = useState([]); + const [isRunning, setIsRunning] = useState(false); + + const addTask = useCallback((task: ITask) => { + setTasks(prevTasks => [...prevTasks, task]); + }, []); + + const removeTask = useCallback((id: string) => { + setTasks(prevTasks => prevTasks.filter(task => task.id !== id)); + }, []); + + const runTasks = useCallback(async () => { + if (isRunning) return; + + setIsRunning(true); + await Promise.allSettled(tasks.map(task => task.run())); + setIsRunning(false); + }, [tasks, isRunning]); + + const context = useMemo( + () => ({ + tasks, + isRunning, + addTask, + removeTask, + runTasks, + }), + [tasks, isRunning, addTask, removeTask, runTasks], + ); + + return {children}; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/TaskRunner.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/TaskRunner.unit.test.tsx new file mode 100644 index 0000000000..c11f639d68 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/TaskRunner.unit.test.tsx @@ -0,0 +1,147 @@ +import { render, renderHook } from '@testing-library/react'; +import { useContext } from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { TaskRunnerContext } from './context'; +import { TaskRunner } from './TaskRunner'; +import { ITask } from './types'; + +describe('TaskRunner', () => { + it('should initialize with empty tasks and not running', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + const { result } = renderHook(() => useContext(TaskRunnerContext), { wrapper }); + + expect(result.current.tasks).toEqual([]); + expect(result.current.isRunning).toBe(false); + }); + + it('should add task', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + const { result, rerender } = renderHook(() => useContext(TaskRunnerContext), { wrapper }); + + const mockTask: ITask = { + id: '1', + element: {} as any, + run: vi.fn(), + }; + + result.current.addTask(mockTask); + rerender(); + expect(result.current.tasks).toHaveLength(1); + expect(result.current.tasks[0]).toEqual(mockTask); + }); + + it('should remove task', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + const { result, rerender } = renderHook(() => useContext(TaskRunnerContext), { wrapper }); + + const mockTask: ITask = { + id: '1', + element: {} as any, + run: vi.fn(), + }; + + result.current.addTask(mockTask); + + rerender(); + expect(result.current.tasks).toHaveLength(1); + + result.current.removeTask('1'); + rerender(); + expect(result.current.tasks).toHaveLength(0); + }); + + it('should run tasks', async () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + const { result, rerender } = renderHook(() => useContext(TaskRunnerContext), { wrapper }); + + const mockTask1: ITask = { + id: '1', + element: {} as any, + run: vi.fn().mockResolvedValue(undefined), + }; + + const mockTask2: ITask = { + id: '2', + element: {} as any, + run: vi.fn().mockResolvedValue(undefined), + }; + + result.current.addTask(mockTask1); + result.current.addTask(mockTask2); + + rerender(); + + await result.current.runTasks(); + + expect(mockTask1.run).toHaveBeenCalled(); + expect(mockTask2.run).toHaveBeenCalled(); + expect(result.current.isRunning).toBe(false); + }); + + it('should not run tasks if already running', async () => { + vi.useFakeTimers(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + const { result, rerender } = renderHook(() => useContext(TaskRunnerContext), { wrapper }); + + const mockTask: ITask = { + id: '1', + element: {} as any, + run: vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100))), + }; + + result.current.addTask(mockTask); + + rerender(); + + // Start first run + const firstRun = result.current.runTasks(); + + rerender(); + + // Verify isRunning is true + expect(result.current.isRunning).toBe(true); + + // Try to run again while first is still running + const secondRun = result.current.runTasks(); + + // Advance timers to resolve the promises + vi.advanceTimersByTime(100); + + await firstRun; + await secondRun; + + rerender(); + + // Verify task only ran once and isRunning is false + expect(mockTask.run).toHaveBeenCalledTimes(1); + expect(result.current.isRunning).toBe(false); + + vi.useRealTimers(); + }); + + it('should render children', () => { + const { getByText } = render( + +
Test Child
+
, + ); + + expect(getByText('Test Child')).toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/context/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/context/index.ts new file mode 100644 index 0000000000..50d69bb16a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/context/index.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import { ITaskRunnerContext } from '../types'; + +export const TaskRunnerContext = createContext({} as ITaskRunnerContext); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/index.ts new file mode 100644 index 0000000000..a2b0105421 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/index.ts @@ -0,0 +1 @@ +export * from './useTaskRunner'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/useTaskRunner.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/useTaskRunner.ts new file mode 100644 index 0000000000..8d9d3903c4 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/useTaskRunner.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { TaskRunnerContext } from '../../context'; + +export const useTaskRunner = () => useContext(TaskRunnerContext); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/useTaskRunner.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/useTaskRunner.unit.test.tsx new file mode 100644 index 0000000000..9d359478a1 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/hooks/useTaskRunner/useTaskRunner.unit.test.tsx @@ -0,0 +1,30 @@ +import { renderHook } from '@testing-library/react'; +import { useContext } from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { useTaskRunner } from './useTaskRunner'; + +vi.mock('react', () => ({ + useContext: vi.fn(), +})); + +vi.mock('../../context', () => ({ + TaskRunnerContext: vi.fn(), +})); + +describe('useTaskRunner', () => { + it('should return context from TaskRunnerContext', () => { + const mockContext = { + tasks: [], + isRunning: false, + addTask: vi.fn(), + removeTask: vi.fn(), + runTasks: vi.fn(), + }; + + vi.mocked(useContext).mockReturnValue(mockContext); + + const { result } = renderHook(() => useTaskRunner()); + + expect(result.current).toBe(mockContext); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/index.ts new file mode 100644 index 0000000000..30b81347c6 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/index.ts @@ -0,0 +1 @@ +export * from './TaskRunner'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/types/index.ts new file mode 100644 index 0000000000..8988e0c8e4 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/TaskRunner/types/index.ts @@ -0,0 +1,15 @@ +import { IFormElement } from '../../../types'; + +export interface ITask { + id: string; + element: IFormElement; + run: () => Promise; +} + +export interface ITaskRunnerContext { + tasks: ITask[]; + isRunning: boolean; + addTask: (task: ITask) => void; + removeTask: (id: string) => void; + runTasks: () => Promise; +} From 949373a725a7c27435bfdd48d903303b139141d8 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Thu, 19 Dec 2024 16:20:38 +0200 Subject: [PATCH 27/54] feat: implemented file upload on submit & tests --- .../FileUploadShowcase/FileUploadShowcase.tsx | 16 +- .../controls/SubmitButton/SubmitButton.tsx | 10 +- .../SubmitButton/SubmitButton.unit.test.tsx | 242 +++++++++++------- .../fields/FileField/FileField.tsx | 19 +- .../fields/FileField/FileField.unit.test.tsx | 160 ++++++++++++ .../hooks/useFileUpload/useFileUpload.ts | 40 ++- .../useFileUpload/useFileUpload.unit.test.ts | 171 +++++++++++++ .../useRuleEngine/useRuleEngine.unit.test.ts | 2 +- 8 files changed, 549 insertions(+), 111 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx index d84849f57f..17c25df649 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/FileUploadShowcase/FileUploadShowcase.tsx @@ -23,7 +23,7 @@ const schema: Array> = [ element: 'filefield', valueDestination: 'file-protected', params: { - label: 'Upload on Submit', + label: 'Upload to protected endpoint', placeholder: 'Select File', uploadSettings: { url: 'http://localhost:3000/upload-protected', @@ -34,6 +34,20 @@ const schema: Array> = [ }, }, }, + { + id: 'FileField:SubmitUpload', + element: 'filefield', + valueDestination: 'file-submit-upload', + params: { + label: 'Upload on Submit', + placeholder: 'Select File', + uploadOn: 'submit', + uploadSettings: { + url: 'http://localhost:3000/upload', + resultPath: 'filename', + }, + }, + }, { id: 'SubmitButton', element: 'submitbutton', 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 32c17997d6..1a90663dc1 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 @@ -4,6 +4,7 @@ import { useValidator } from '../../../Validator'; import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external/useElement'; import { useField } from '../../hooks/external/useField'; +import { useTaskRunner } from '../../providers/TaskRunner/hooks/useTaskRunner'; import { TDynamicFormElement } from '../../types'; export interface ISubmitButtonParams { @@ -15,6 +16,7 @@ export const SubmitButton: TDynamicFormElement = ({ const { id } = useElement(element); const { disabled: _disabled } = useField(element); const { fieldHelpers, submit } = useDynamicForm(); + const { runTasks } = useTaskRunner(); const { touchAllFields } = fieldHelpers; @@ -28,13 +30,17 @@ export const SubmitButton: TDynamicFormElement = ({ return _disabled; }, [disableWhenFormIsInvalid, isValid, _disabled]); - const handleSubmit = useCallback(() => { + const handleSubmit = useCallback(async () => { touchAllFields(); if (!isValid) return; + console.log('Starting tasks'); + await runTasks(); + console.log('Tasks finished'); + submit(); - }, [submit, isValid, touchAllFields]); + }, [submit, isValid, touchAllFields, runTasks]); return ( ), })); +vi.mock('../../../Validator'); vi.mock('../../context'); - +vi.mock('../../hooks/external/useElement'); vi.mock('../../hooks/external/useField'); -vi.mock('../../hooks/internal/useFieldHelpers'); +vi.mock('../../providers/TaskRunner/hooks/useTaskRunner'); describe('SubmitButton', () => { const mockElement = { id: 'test-button', - params: {}, - valueDestination: 'test.path', - element: '', - } as unknown as IFormElement; - - const mockSubmit = vi.fn(); + params: { + disableWhenFormIsInvalid: false, + text: 'Test Submit', + }, + } as IFormElement; + + const mockFieldHelpers = { + touchAllFields: vi.fn(), + getTouched: vi.fn(), + getValue: vi.fn(), + setTouched: vi.fn(), + setValue: vi.fn(), + }; beforeEach(() => { - cleanup(); - vi.clearAllMocks(); - - const mockSubmit = vi.fn(); - - vi.mocked(Button).mockImplementation(({ children, ...props }) => ( - - )); - - vi.mocked(useFieldHelpers).mockReturnValue({ - touchAllFields: vi.fn(), - } as any); + vi.mocked(useElement).mockReturnValue({ + id: 'test-button', + originId: 'test-button', + hidden: false, + }); + vi.mocked(useField).mockReturnValue({ + disabled: false, + value: null, + touched: false, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + }); vi.mocked(useDynamicForm).mockReturnValue({ - submit: mockSubmit, - fieldHelpers: { - touchAllFields: vi.fn(), - }, - } as any); - - vi.mocked(useField).mockReturnValue({ disabled: false } as any); - - vi.mocked(useValidator).mockReturnValue({ isValid: true } as any); - vi.mocked(useElement).mockReturnValue({ id: 'test-id' } as any); + fieldHelpers: mockFieldHelpers, + submit: vi.fn(), + values: {}, + touched: {}, + elementsMap: {}, + callbacks: {}, + metadata: {}, + } as IDynamicFormContext); + vi.mocked(useTaskRunner).mockReturnValue({ + tasks: [], + isRunning: false, + addTask: vi.fn(), + removeTask: vi.fn(), + runTasks: vi.fn(), + } as ITaskRunnerContext); + vi.mocked(useValidator).mockReturnValue({ + isValid: true, + errors: {}, + values: {}, + validate: vi.fn(), + } as unknown as IValidatorContext); }); afterEach(() => { - vi.restoreAllMocks(); + cleanup(); + vi.clearAllMocks(); }); - it('should render button with default text', () => { + it('renders with default props', () => { render(); - expect(screen.getByText('Submit')).toBeInTheDocument(); + + const button = screen.getByTestId('test-button-submit-button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveTextContent('Test Submit'); }); - it('should render button with custom text', () => { - const elementWithText = { + it('disables button when form is invalid and disableWhenFormIsInvalid is true', () => { + const element = { ...mockElement, - params: { text: 'Custom Submit' }, + params: { disableWhenFormIsInvalid: true }, }; - render(); - expect(screen.getByText('Custom Submit')).toBeInTheDocument(); - }); - - describe('disabled state', () => { - it('should be disabled when useField returns disabled true', () => { - vi.mocked(useField).mockReturnValue({ disabled: true } as any); + vi.mocked(useValidator).mockReturnValue({ + isValid: false, + errors: {}, + values: {}, + validate: vi.fn(), + } as unknown as IValidatorContext); - render(); + render(); - expect(screen.getByRole('button')).toBeDisabled(); - }); + expect(screen.getByTestId('test-button-submit-button')).toBeDisabled(); + }); - it('should be disabled when form is invalid and disableWhenFormIsInvalid is true', () => { - vi.mocked(useValidator).mockReturnValue({ isValid: false } as any); + it('handles submit when form is valid', async () => { + const mockSubmit = vi.fn(); + const mockRunTasks = vi.fn(); - const elementWithDisable = { - ...mockElement, - params: { disableWhenFormIsInvalid: true }, - }; + vi.mocked(useDynamicForm).mockReturnValue({ + fieldHelpers: mockFieldHelpers, + submit: mockSubmit, + values: {}, + touched: {}, + elementsMap: {}, + callbacks: {}, + metadata: {}, + } as IDynamicFormContext); + vi.mocked(useTaskRunner).mockReturnValue({ + tasks: [], + isRunning: false, + addTask: vi.fn(), + removeTask: vi.fn(), + runTasks: mockRunTasks, + }); + vi.mocked(useValidator).mockReturnValue({ + isValid: true, + errors: [], + values: {}, + validate: vi.fn(), + }); - render(); + render(); - expect(screen.getByRole('button')).toBeDisabled(); - }); + await userEvent.click(screen.getByTestId('test-button-submit-button')); - it('should not be disabled when form is invalid but disableWhenFormIsInvalid is false', () => { - vi.mocked(useValidator).mockReturnValue({ isValid: false } as any); - vi.mocked(useField).mockReturnValue({ disabled: false } as any); + expect(mockFieldHelpers.touchAllFields).toHaveBeenCalled(); + expect(mockRunTasks).toHaveBeenCalled(); + expect(mockSubmit).toHaveBeenCalled(); + }); - render(); + it('does not submit when form is invalid', async () => { + const mockSubmit = vi.fn(); + const mockRunTasks = vi.fn(); - expect(screen.getByRole('button')).not.toBeDisabled(); + vi.mocked(useDynamicForm).mockReturnValue({ + fieldHelpers: mockFieldHelpers, + submit: mockSubmit, + values: {}, + touched: {}, + elementsMap: {}, + callbacks: {}, + metadata: {}, + } as IDynamicFormContext); + vi.mocked(useTaskRunner).mockReturnValue({ + tasks: [], + isRunning: false, + addTask: vi.fn(), + removeTask: vi.fn(), + runTasks: mockRunTasks, }); + vi.mocked(useValidator).mockReturnValue({ + isValid: false, + errors: [], + values: {}, + validate: vi.fn(), + } as unknown as IValidatorContext); - it('should not call submit when form is invalid and disableWhenFormIsInvalid is true', async () => { - vi.mocked(useValidator).mockReturnValue({ isValid: false } as any); - - const elementWithDisable = { - ...mockElement, - params: { disableWhenFormIsInvalid: true }, - }; + render(); - render(); - await userEvent.click(screen.getByRole('button')); + await userEvent.click(screen.getByTestId('test-button-submit-button')); - expect(mockSubmit).not.toHaveBeenCalled(); - }); + expect(mockFieldHelpers.touchAllFields).toHaveBeenCalled(); + expect(mockRunTasks).not.toHaveBeenCalled(); + expect(mockSubmit).not.toHaveBeenCalled(); }); - it('should have correct test id', () => { - render(); - expect(screen.getByTestId('test-id-submit-button')).toBeInTheDocument(); - }); + it('uses default text when not provided', () => { + const element = { + ...mockElement, + params: {}, + }; - it('should render with secondary variant', () => { - render(); - expect(vi.mocked(Button).mock.calls[0]?.[0]?.variant).toBe('secondary'); + render(); + + expect(screen.getByTestId('test-button-submit-button')).toHaveTextContent('Submit'); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx index 3610b3f916..047d2d6306 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx @@ -29,7 +29,10 @@ export const FileField: TDynamicFormField = ({ element }) => { useUnmountEvent(element); const { placeholder = 'Choose file', acceptFileFormats = undefined } = element.params || {}; - const { handleChange } = useFileUpload(element, element.params); + const { handleChange, isUploading: disabledWhileUploading } = useFileUpload( + element, + element.params, + ); const { stack } = useStack(); const { value, disabled, onChange, onBlur, onFocus } = useField( @@ -50,12 +53,20 @@ export const FileField: TDynamicFormField = ({ element }) => { return undefined; }, [value]); + const clearFileAndInput = useCallback(() => { + onChange(undefined); + + if (inputRef.current) { + inputRef.current.value = ''; + } + }, [onChange]); + return (
= ({ element }) => { className="h-[28px] w-[28px] rounded-full" onClick={e => { e.stopPropagation(); - onChange(undefined); + clearFileAndInput(); }} >
@@ -85,7 +96,7 @@ export const FileField: TDynamicFormField = ({ element }) => { type="file" placeholder={placeholder} accept={acceptFileFormats} - disabled={disabled} + disabled={disabled || disabledWhileUploading} onChange={handleChange} onBlur={onBlur} onFocus={onFocus} diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx new file mode 100644 index 0000000000..9667b4b8c3 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx @@ -0,0 +1,160 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useField } from '../../hooks/external'; +import { useMountEvent } from '../../hooks/internal/useMountEvent'; +import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { IFormElement } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; +import { FileField, IFileFieldParams } from './FileField'; +import { useFileUpload } from './hooks/useFileUpload'; + +vi.mock('../../hooks/external'); +vi.mock('../../hooks/internal/useMountEvent'); +vi.mock('../../hooks/internal/useUnmountEvent'); +vi.mock('../FieldList/providers/StackProvider'); +vi.mock('./hooks/useFileUpload'); +vi.mock('@/components/atoms', () => ({ + Button: vi.fn(({ children, onClick, ...props }) => ( + + )), + Input: vi.fn(({ ...props }, ref) => ), +})); +vi.mock('../../layouts/FieldLayout', () => ({ + FieldLayout: vi.fn(({ children }) =>
{children}
), +})); +vi.mock('../../layouts/FieldErrors', () => ({ + FieldErrors: vi.fn(({ element }) =>
{element.id}
), +})); + +describe('FileField', () => { + const mockElement = { + id: 'test-file', + params: { + placeholder: 'Test Placeholder', + acceptFileFormats: '.jpg,.png', + }, + } as IFormElement; + + const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' }); + + beforeEach(() => { + vi.mocked(useStack).mockReturnValue({ + stack: [], + }); + + vi.mocked(useField).mockReturnValue({ + value: undefined, + disabled: false, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }); + + vi.mocked(useFileUpload).mockReturnValue({ + handleChange: vi.fn(), + isUploading: false, + }); + }); + + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + it('renders with default props', () => { + render(); + + expect(screen.getByText('Test Placeholder')).toBeInTheDocument(); + expect(screen.getByText('No File Choosen')).toBeInTheDocument(); + }); + + it('shows file name when file is selected', () => { + vi.mocked(useField).mockReturnValue({ + value: mockFile, + disabled: false, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }); + + render(); + + expect(screen.getByText('test.txt')).toBeInTheDocument(); + }); + + it('shows clear button when file is selected', () => { + const mockOnChange = vi.fn(); + vi.mocked(useField).mockReturnValue({ + value: mockFile, + disabled: false, + onChange: mockOnChange, + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }); + + render(); + + const clearButton = screen.getByRole('button'); + expect(clearButton).toBeInTheDocument(); + }); + + it('clears file when clear button is clicked', async () => { + const mockOnChange = vi.fn(); + vi.mocked(useField).mockReturnValue({ + value: mockFile, + disabled: false, + onChange: mockOnChange, + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }); + + render(); + + const clearButton = screen.getByRole('button'); + await userEvent.click(clearButton); + + expect(mockOnChange).toHaveBeenCalledWith(undefined); + }); + + it('disables input when field is disabled', () => { + vi.mocked(useField).mockReturnValue({ + value: undefined, + disabled: true, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + touched: false, + }); + + render(); + + const container = screen.getByTestId('test-file'); + expect(container.className).toContain('pointer-events-none'); + }); + + it('disables input while uploading', () => { + vi.mocked(useFileUpload).mockReturnValue({ + handleChange: vi.fn(), + isUploading: true, + }); + + render(); + + const container = screen.getByTestId('test-file'); + expect(container.className).toContain('pointer-events-none'); + }); + + it('calls mount and unmount events', () => { + render(); + + expect(useMountEvent).toHaveBeenCalledWith(mockElement); + expect(useUnmountEvent).toHaveBeenCalledWith(mockElement); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts index e0446de3a7..aa008c76d6 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts @@ -1,7 +1,10 @@ import { useCallback, useState } from 'react'; import { useDynamicForm } from '../../../../context'; -import { useField } from '../../../../hooks/external'; +import { useElement, useField } from '../../../../hooks/external'; +import { useTaskRunner } from '../../../../providers/TaskRunner/hooks/useTaskRunner'; +import { ITask } from '../../../../providers/TaskRunner/types'; import { IFormElement } from '../../../../types'; +import { useStack } from '../../../FieldList/providers/StackProvider'; import { IFileFieldParams } from '../../FileField'; import { formatHeaders, uploadFile } from './helpers'; @@ -10,6 +13,9 @@ export const useFileUpload = ( params: IFileFieldParams = {}, ) => { const { uploadOn = 'change' } = params; + const { stack } = useStack(); + const { id } = useElement(element, stack); + const { addTask, removeTask } = useTaskRunner(); const [isUploading, setIsUploading] = useState(false); const { metadata } = useDynamicForm(); @@ -17,6 +23,8 @@ export const useFileUpload = ( const handleChange = useCallback( async (e: React.ChangeEvent) => { + removeTask(id); + const { uploadSettings } = params; const uploadParams = { @@ -25,8 +33,6 @@ export const useFileUpload = ( headers: formatHeaders(uploadSettings?.headers || {}, metadata), }; - console.log('uploadParams', uploadParams); - if (uploadOn === 'change') { try { setIsUploading(true); @@ -42,8 +48,34 @@ export const useFileUpload = ( setIsUploading(false); } } + + if (uploadOn === 'submit') { + onChange(e.target?.files?.[0] as File); + + const taskRun = async () => { + try { + setIsUploading(true); + const result = await uploadFile( + e.target?.files?.[0] as File, + uploadParams as IFileFieldParams['uploadSettings'], + ); + onChange(result); + } catch (error) { + console.error('Failed to upload file.', error); + } finally { + setIsUploading(false); + } + }; + + const task: ITask = { + id, + element, + run: taskRun, + }; + addTask(task); + } }, - [uploadOn, params, metadata, onChange], + [uploadOn, params, metadata, addTask, removeTask, onChange, id, element], ); return { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts new file mode 100644 index 0000000000..afbe738769 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts @@ -0,0 +1,171 @@ +import { cleanup, renderHook } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useDynamicForm } from '../../../../context'; +import { useElement, useField } from '../../../../hooks/external'; +import { useTaskRunner } from '../../../../providers/TaskRunner/hooks/useTaskRunner'; +import { IFormElement } from '../../../../types'; +import { useStack } from '../../../FieldList/providers/StackProvider'; +import { IFileFieldParams } from '../../FileField'; +import { formatHeaders, uploadFile } from './helpers'; +import { useFileUpload } from './useFileUpload'; + +vi.mock('../../../../context'); +vi.mock('../../../../hooks/external'); +vi.mock('../../../../providers/TaskRunner/hooks/useTaskRunner'); +vi.mock('../../../FieldList/providers/StackProvider'); +vi.mock('./helpers'); + +describe('useFileUpload', () => { + const mockElement = { + id: 'test-file', + params: { + uploadSettings: { + url: 'test-url', + resultPath: 'test-path', + }, + }, + } as IFormElement; + + const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' }); + const mockEvent = { + target: { + files: [mockFile], + }, + } as unknown as React.ChangeEvent; + + beforeEach(() => { + vi.mocked(useStack).mockReturnValue({ + stack: [], + }); + + vi.mocked(useElement).mockReturnValue({ + id: 'test-file', + originId: 'test-file', + hidden: false, + }); + + vi.mocked(useTaskRunner).mockReturnValue({ + addTask: vi.fn(), + removeTask: vi.fn(), + tasks: [], + isRunning: false, + runTasks: vi.fn(), + }); + + vi.mocked(useDynamicForm).mockReturnValue({ + metadata: {}, + fieldHelpers: { + getTouched: vi.fn(), + getValue: vi.fn(), + setTouched: vi.fn(), + setValue: vi.fn(), + touchAllFields: vi.fn(), + }, + values: {}, + touched: {}, + elementsMap: {}, + callbacks: {}, + submit: vi.fn(), + }); + + vi.mocked(useField).mockReturnValue({ + value: null, + touched: false, + disabled: false, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + }); + + vi.mocked(uploadFile).mockResolvedValue('uploaded-file-result'); + vi.mocked(formatHeaders).mockReturnValue({}); + }); + + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + it('handles file upload on change', async () => { + const { result } = renderHook(() => useFileUpload(mockElement, { uploadOn: 'change' })); + + await result.current.handleChange(mockEvent); + + expect(vi.mocked(uploadFile)).toHaveBeenCalledWith(mockFile, expect.any(Object)); + expect(vi.mocked(useField).mock.results?.[0]?.value?.onChange).toHaveBeenCalledWith( + 'uploaded-file-result', + ); + }); + + it('handles file upload on submit', async () => { + const { result } = renderHook(() => useFileUpload(mockElement, { uploadOn: 'submit' })); + + await result.current.handleChange(mockEvent); + + expect(vi.mocked(useField).mock.results?.[0]?.value?.onChange).toHaveBeenCalledWith(mockFile); + expect(vi.mocked(useTaskRunner).mock.results?.[0]?.value?.addTask).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'test-file', + element: mockElement, + run: expect.any(Function), + }), + ); + }); + + it('removes existing task before handling new file', async () => { + const { result } = renderHook(() => useFileUpload(mockElement)); + + await result.current.handleChange(mockEvent); + + expect(vi.mocked(useTaskRunner).mock.results?.[0]?.value?.removeTask).toHaveBeenCalledWith( + 'test-file', + ); + }); + + it('handles upload failure gracefully', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => void 0); + vi.mocked(uploadFile).mockRejectedValueOnce(new Error('Upload failed')); + + const { result } = renderHook(() => useFileUpload(mockElement, { uploadOn: 'change' })); + + await result.current.handleChange(mockEvent); + + expect(consoleError).toHaveBeenCalledWith('Failed to upload file.', expect.any(Error)); + consoleError.mockRestore(); + }); + + it('formats headers with metadata', async () => { + vi.mocked(useDynamicForm).mockReturnValue({ + metadata: { token: '123' }, + fieldHelpers: { + getTouched: vi.fn(), + getValue: vi.fn(), + setTouched: vi.fn(), + setValue: vi.fn(), + touchAllFields: vi.fn(), + }, + values: {}, + touched: {}, + elementsMap: {}, + callbacks: {}, + submit: vi.fn(), + }); + + const { result } = renderHook(() => + useFileUpload(mockElement, { + uploadSettings: { + headers: { Authorization: 'Bearer {token}' }, + url: 'test-url', + resultPath: 'test-path', + }, + }), + ); + + await result.current.handleChange(mockEvent); + + expect(vi.mocked(formatHeaders)).toHaveBeenCalledWith( + { Authorization: 'Bearer {token}' }, + { token: '123' }, + ); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts index ffd5c850c6..51e7f976b7 100644 --- a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts @@ -46,7 +46,7 @@ describe('useRuleEngine', () => { const { result } = renderHook(() => useRuleEngine(context, { rules, executeRulesSync: false })); // Wait for debounced execution - await vi.advanceTimersByTimeAsync(500); + await vi.advanceTimersByTimeAsync(550); // Assert expect(result.current).toEqual(expectedResults); From 12534dc6aa37b72ebb24a8e388bfcfa82c7c95a1 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 20 Dec 2024 11:33:51 +0200 Subject: [PATCH 28/54] feat: added more form stories & tests update --- .../Form/DynamicForm/DynamicForm.stories.tsx | 10 + .../ConditionalRenderingShowcase.tsx | 28 +++ .../ConditionalRenderingShowcase/index.ts | 1 + .../ConditionalRenderingShowcase/schema.ts | 80 ++++++ .../ValidationShowcase/ValidationShowcase.tsx | 28 +++ .../_stories/ValidationShowcase/index.ts | 0 .../_stories/ValidationShowcase/schema.ts | 136 ++++++++++ .../hooks/useFileUpload/useFileUpload.ts | 7 + .../useFileUpload/useFileUpload.unit.test.ts | 237 ++++++++---------- .../check-if-required/check-if-required.ts | 12 +- .../check-if-required.unit.test.ts | 163 +++++++----- .../useRuleEngine/useRuleEngine.unit.test.ts | 2 +- 12 files changed, 503 insertions(+), 201 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/ConditionalRenderingShowcase.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/ValidationShowcase.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx index 27269ee1bc..62f763ac93 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx @@ -1,6 +1,8 @@ import { DynamicFormV2 } from './DynamicForm'; +import { ConditionalRenderingShowcaseComponent } from './_stories/ConditionalRenderingShowcase'; import { FileUploadShowcaseComponent } from './_stories/FileUploadShowcase'; import { InputsShowcaseComponent } from './_stories/InputsShowcase'; +import { ValidationShowcaseComponent } from './_stories/ValidationShowcase/ValidationShowcase'; export default { component: DynamicFormV2, @@ -13,3 +15,11 @@ export const InputsShowcase = { export const FileUploadShowcase = { render: () => , }; + +export const ValidationShowcase = { + render: () => , +}; + +export const ConditionalRenderingShowcase = { + render: () => , +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/ConditionalRenderingShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/ConditionalRenderingShowcase.tsx new file mode 100644 index 0000000000..d884587cbe --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/ConditionalRenderingShowcase.tsx @@ -0,0 +1,28 @@ +import { AnyObject } from '@/common'; +import { useState } from 'react'; +import { JSONEditorComponent } from '../../../Validator/_stories/components/JsonEditor/JsonEditor'; +import { DynamicFormV2 } from '../../DynamicForm'; +import { schema } from './schema'; + +export const ConditionalRenderingShowcaseComponent = () => { + const [context, setContext] = useState({}); + + return ( +
+
+ { + console.log('onSubmit'); + }} + onChange={setContext} + // onEvent={console.log} + /> +
+
+ +
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/index.ts new file mode 100644 index 0000000000..811fb8fbee --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/index.ts @@ -0,0 +1 @@ +export * from './ConditionalRenderingShowcase'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts new file mode 100644 index 0000000000..44aee0761d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts @@ -0,0 +1,80 @@ +import { IFormElement } from '../../types'; + +export const schema: Array> = [ + { + id: 'first-name', + element: 'textfield', + valueDestination: 'firstName', + params: { + label: 'First Name', + placeholder: 'Enter something to reveal some more!', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'First name is required', + applyWhen: { + type: 'json-logic', + value: { + '!': { var: 'forceEverythingOptionnal' }, + }, + }, + }, + ], + }, + { + id: 'reveal-more', + element: 'checkboxfield', + valueDestination: 'revealMore', + params: { + label: 'Reveal More', + }, + }, + { + id: 'force-everything-optionnal', + element: 'checkboxfield', + valueDestination: 'forceEverythingOptionnal', + params: { + label: 'Force everything to be optionnal', + }, + }, + { + id: 'last-name', + element: 'textfield', + valueDestination: 'lastName', + params: { + label: 'Last Name', + }, + hidden: [ + { + engine: 'json-logic', + value: { + and: [{ '!': { var: 'firstName' } }, { '!': { var: 'revealMore' } }], + }, + }, + ], + validate: [ + { + type: 'required', + value: {}, + message: 'Last name is required', + applyWhen: { + type: 'json-logic', + value: { + and: [{ '!!': { var: 'firstName' } }, { '!': { var: 'forceEverythingOptionnal' } }], + }, + }, + }, + ], + }, + { + id: 'submit', + element: 'submitbutton', + valueDestination: 'submit', + params: { + label: 'Submit', + disableWhenFormIsInvalid: true, + }, + }, +]; 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 new file mode 100644 index 0000000000..e0cf5e3095 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/ValidationShowcase.tsx @@ -0,0 +1,28 @@ +import { AnyObject } from '@/common'; +import { useState } from 'react'; +import { JSONEditorComponent } from '../../../Validator/_stories/components/JsonEditor/JsonEditor'; +import { DynamicFormV2 } from '../../DynamicForm'; +import { schema } from './schema'; + +export const ValidationShowcaseComponent = () => { + const [context, setContext] = useState({}); + + return ( +
+
+ { + console.log('onSubmit'); + }} + onChange={setContext} + // onEvent={console.log} + /> +
+
+ +
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts new file mode 100644 index 0000000000..50c12c7b9c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts @@ -0,0 +1,136 @@ +import { IFormElement } from '../../types'; + +export const schema: Array> = [ + { + id: 'first-name-field', + element: 'textfield', + valueDestination: 'firstName', + params: { + label: 'First Name', + placeholder: 'Enter your first name', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'First name is required', + }, + ], + }, + { + id: 'last-name-field', + element: 'textfield', + valueDestination: 'lastName', + params: { + label: 'Last Name', + placeholder: 'Enter your last name', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'Last name is required', + }, + ], + }, + { + id: 'date-of-birth-field', + element: 'datefield', + valueDestination: 'dateOfBirth', + params: { + label: 'Date of Birth', + placeholder: 'Enter your date of birth', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'Date of birth is required', + }, + ], + }, + { + id: 'passport-photo', + element: 'filefield', + valueDestination: 'passportPhoto', + params: { + label: 'Passport Photo', + placeholder: 'Select your passport photo', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'Passport photo is required', + applyWhen: { + type: 'json-logic', + value: { + '!': { var: 'iDontHaveDocument' }, + }, + }, + }, + ], + }, + { + id: 'idont-have-document-checkbox', + element: 'checkboxfield', + valueDestination: 'iDontHaveDocument', + params: { + label: "I don't have a document", + }, + }, + { + id: 'workplaces', + valueDestination: 'workplaces', + element: 'fieldlist', + params: { + label: 'Workplaces', + addButtonLabel: 'Add Workplace', + }, + validate: [ + { type: 'required', value: {}, message: 'Workplaces are required' }, + { + type: 'minLength', + value: { minLength: 2 }, + message: 'At least {minLength} workplaces are required', + }, + ], + children: [ + { + id: 'workplace-name', + element: 'textfield', + valueDestination: 'workplaces[$0].workplaceName', + params: { + label: 'Workplace Name', + }, + validate: [{ type: 'required', value: {}, message: 'Workplace name is required' }], + }, + { + id: 'workplace-start-date', + element: 'datefield', + valueDestination: 'workplaces[$0].workplaceStartDate', + params: { + label: 'Workplace Start Date', + }, + validate: [{ type: 'required', value: {}, message: 'Workplace start date is required' }], + }, + { + id: 'certificate-of-employment', + element: 'filefield', + valueDestination: 'workplaces[$0].certificateOfEmployment', + params: { + label: 'Certificate of Employment', + }, + validate: [], + }, + ], + }, + { + id: 'submit-button', + element: 'submitbutton', + valueDestination: 'submit', + params: { + label: 'Submit', + }, + }, +]; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts index aa008c76d6..a62703f7a8 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts @@ -27,6 +27,13 @@ export const useFileUpload = ( const { uploadSettings } = params; + if (!uploadSettings) { + onChange(e.target?.files?.[0] as File); + console.log('Failed to upload, no upload settings provided'); + + return; + } + const uploadParams = { ...uploadSettings, method: uploadSettings?.method || 'POST', diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts index afbe738769..d8f3b3b27d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts @@ -1,171 +1,150 @@ -import { cleanup, renderHook } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { useDynamicForm } from '../../../../context'; -import { useElement, useField } from '../../../../hooks/external'; -import { useTaskRunner } from '../../../../providers/TaskRunner/hooks/useTaskRunner'; -import { IFormElement } from '../../../../types'; -import { useStack } from '../../../FieldList/providers/StackProvider'; -import { IFileFieldParams } from '../../FileField'; +import { act, renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { formatHeaders, uploadFile } from './helpers'; import { useFileUpload } from './useFileUpload'; -vi.mock('../../../../context'); -vi.mock('../../../../hooks/external'); -vi.mock('../../../../providers/TaskRunner/hooks/useTaskRunner'); -vi.mock('../../../FieldList/providers/StackProvider'); -vi.mock('./helpers'); +vi.mock('./helpers', () => ({ + uploadFile: vi.fn(), + formatHeaders: vi.fn(), +})); -describe('useFileUpload', () => { - const mockElement = { - id: 'test-file', - params: { - uploadSettings: { - url: 'test-url', - resultPath: 'test-path', - }, - }, - } as IFormElement; +vi.mock('../../../../context', () => ({ + useDynamicForm: () => ({ + metadata: { test: 'metadata' }, + }), +})); - const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' }); - const mockEvent = { - target: { - files: [mockFile], - }, - } as unknown as React.ChangeEvent; +vi.mock('../../../../hooks/external', () => ({ + useElement: () => ({ id: 'test-id' }), + useField: () => ({ onChange: vi.fn() }), +})); - beforeEach(() => { - vi.mocked(useStack).mockReturnValue({ - stack: [], - }); +vi.mock('../../../../providers/TaskRunner/hooks/useTaskRunner', () => ({ + useTaskRunner: () => ({ + addTask: vi.fn(), + removeTask: vi.fn(), + }), +})); - vi.mocked(useElement).mockReturnValue({ - id: 'test-file', - originId: 'test-file', - hidden: false, - }); +vi.mock('../../../FieldList/providers/StackProvider', () => ({ + useStack: () => ({ stack: [] }), +})); - vi.mocked(useTaskRunner).mockReturnValue({ - addTask: vi.fn(), - removeTask: vi.fn(), - tasks: [], - isRunning: false, - runTasks: vi.fn(), - }); +const mockedUploadFile = vi.mocked(uploadFile); +const mockedFormatHeaders = vi.mocked(formatHeaders); - vi.mocked(useDynamicForm).mockReturnValue({ - metadata: {}, - fieldHelpers: { - getTouched: vi.fn(), - getValue: vi.fn(), - setTouched: vi.fn(), - setValue: vi.fn(), - touchAllFields: vi.fn(), +describe('useFileUpload', () => { + const mockElement = { + id: 'test-field', + element: 'file', + valueDestination: 'file', + }; + + const createEvent = (file: File) => + ({ + target: { + files: [file], }, - values: {}, - touched: {}, - elementsMap: {}, - callbacks: {}, - submit: vi.fn(), - }); - - vi.mocked(useField).mockReturnValue({ - value: null, - touched: false, - disabled: false, - onChange: vi.fn(), - onBlur: vi.fn(), - onFocus: vi.fn(), - }); - - vi.mocked(uploadFile).mockResolvedValue('uploaded-file-result'); - vi.mocked(formatHeaders).mockReturnValue({}); - }); + } as unknown as React.ChangeEvent); - afterEach(() => { - cleanup(); + beforeEach(() => { vi.clearAllMocks(); }); - it('handles file upload on change', async () => { - const { result } = renderHook(() => useFileUpload(mockElement, { uploadOn: 'change' })); + it('should handle file change without upload settings', async () => { + const { result } = renderHook(() => useFileUpload(mockElement, {})); + const mockFile = new File(['test'], 'test.txt'); - await result.current.handleChange(mockEvent); + await act(async () => { + await result.current.handleChange(createEvent(mockFile)); + }); - expect(vi.mocked(uploadFile)).toHaveBeenCalledWith(mockFile, expect.any(Object)); - expect(vi.mocked(useField).mock.results?.[0]?.value?.onChange).toHaveBeenCalledWith( - 'uploaded-file-result', - ); + expect(mockedUploadFile).not.toHaveBeenCalled(); }); - it('handles file upload on submit', async () => { - const { result } = renderHook(() => useFileUpload(mockElement, { uploadOn: 'submit' })); + it('should upload file immediately when uploadOn is "change"', async () => { + const uploadSettings = { + url: 'test-url', + resultPath: 'data.url', + headers: { 'Content-Type': 'application/json' }, + }; - await result.current.handleChange(mockEvent); + mockedUploadFile.mockResolvedValue('uploaded-file-url'); + mockedFormatHeaders.mockReturnValue({ 'Content-Type': 'application/json' }); - expect(vi.mocked(useField).mock.results?.[0]?.value?.onChange).toHaveBeenCalledWith(mockFile); - expect(vi.mocked(useTaskRunner).mock.results?.[0]?.value?.addTask).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'test-file', - element: mockElement, - run: expect.any(Function), + const { result } = renderHook(() => + useFileUpload(mockElement, { + uploadOn: 'change', + uploadSettings, }), ); - }); - it('removes existing task before handling new file', async () => { - const { result } = renderHook(() => useFileUpload(mockElement)); + const mockFile = new File(['test'], 'test.txt'); - await result.current.handleChange(mockEvent); + await act(async () => { + await result.current.handleChange(createEvent(mockFile)); + }); - expect(vi.mocked(useTaskRunner).mock.results?.[0]?.value?.removeTask).toHaveBeenCalledWith( - 'test-file', + expect(mockedUploadFile).toHaveBeenCalledWith( + mockFile, + expect.objectContaining({ + url: 'test-url', + method: 'POST', + resultPath: 'data.url', + headers: { 'Content-Type': 'application/json' }, + }), ); }); - it('handles upload failure gracefully', async () => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => void 0); - vi.mocked(uploadFile).mockRejectedValueOnce(new Error('Upload failed')); + it('should queue upload task when uploadOn is "submit"', async () => { + const uploadSettings = { + url: 'test-url', + resultPath: 'data.url', + headers: { 'Content-Type': 'application/json' }, + }; - const { result } = renderHook(() => useFileUpload(mockElement, { uploadOn: 'change' })); + const { result } = renderHook(() => + useFileUpload(mockElement, { + uploadOn: 'submit', + uploadSettings, + }), + ); - await result.current.handleChange(mockEvent); + const mockFile = new File(['test'], 'test.txt'); - expect(consoleError).toHaveBeenCalledWith('Failed to upload file.', expect.any(Error)); - consoleError.mockRestore(); + await act(async () => { + await result.current.handleChange(createEvent(mockFile)); + }); + + expect(mockedUploadFile).not.toHaveBeenCalled(); }); - it('formats headers with metadata', async () => { - vi.mocked(useDynamicForm).mockReturnValue({ - metadata: { token: '123' }, - fieldHelpers: { - getTouched: vi.fn(), - getValue: vi.fn(), - setTouched: vi.fn(), - setValue: vi.fn(), - touchAllFields: vi.fn(), - }, - values: {}, - touched: {}, - elementsMap: {}, - callbacks: {}, - submit: vi.fn(), - }); + it('should handle upload errors gracefully', async () => { + const uploadSettings = { + url: 'test-url', + resultPath: 'data.url', + headers: { 'Content-Type': 'application/json' }, + }; + + mockedUploadFile.mockRejectedValue(new Error('Upload failed')); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(vi.fn() as any); const { result } = renderHook(() => useFileUpload(mockElement, { - uploadSettings: { - headers: { Authorization: 'Bearer {token}' }, - url: 'test-url', - resultPath: 'test-path', - }, + uploadOn: 'change', + uploadSettings, }), ); - await result.current.handleChange(mockEvent); + const mockFile = new File(['test'], 'test.txt'); - expect(vi.mocked(formatHeaders)).toHaveBeenCalledWith( - { Authorization: 'Bearer {token}' }, - { token: '123' }, - ); + await act(async () => { + await result.current.handleChange(createEvent(mockFile)); + }); + + expect(consoleSpy).toHaveBeenCalledWith('Failed to upload file.', expect.any(Error)); + expect(result.current.isUploading).toBe(false); + + consoleSpy.mockRestore(); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts index 925ce02297..e1fbac6044 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts @@ -1,4 +1,3 @@ -import { IRule } from '@/components/organisms/Form/hooks'; import { executeRules } from '@/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules'; import { IFormElement } from '../../../../../types'; @@ -11,9 +10,14 @@ export const checkIfRequired = (element: IFormElement, context: object) => { const isRequired = requiredLikeValidators.length ? requiredLikeValidators.some(validator => { - const { applyWhen = [] } = validator; - const shouldValidate = (applyWhen as IRule[])?.length - ? executeRules(context, applyWhen as IRule[]).every(result => result.result) + const { applyWhen } = validator; + const shouldValidate = applyWhen + ? executeRules(context, [ + { + engine: applyWhen.type, + value: applyWhen.value, + }, + ]).every(result => result.result) : true; if (!shouldValidate) return false; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts index dcdf9c17a7..f5eb3725ef 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts @@ -1,120 +1,149 @@ import { IRuleExecutionResult } from '@/components/organisms/Form/hooks'; import { executeRules } from '@/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { TBaseValidators } from '@/components/organisms/Form/Validator'; +import { describe, expect, it, vi } from 'vitest'; import { IFormElement } from '../../../../../types'; import { checkIfRequired } from './check-if-required'; -vi.mock('@/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules', () => ({ - executeRules: vi.fn(), -})); +vi.mock('@/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules'); -describe('checkIfRequired', () => { - const mockContext = { someContext: 'value' }; - - beforeEach(() => { - vi.clearAllMocks(); - }); +const mockedExecuteRules = vi.mocked(executeRules); - it('should return false when validate array is empty', () => { - const element = { +describe('checkIfRequired', () => { + it('should return false when there are no validators', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', validate: [], - } as unknown as IFormElement; + }; - const result = checkIfRequired(element, mockContext); + const result = checkIfRequired(element, {}); expect(result).toBe(false); }); - it('should return false when no required validators exist', () => { - const element = { + it('should return false when there are no required validators', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', validate: [ - { type: 'minLength', value: 5 }, - { type: 'maxLength', value: 10 }, + { + type: 'custom' as TBaseValidators, + value: {}, + message: 'Custom message', + }, ], - } as unknown as IFormElement; + }; - const result = checkIfRequired(element, mockContext); + const result = checkIfRequired(element, {}); expect(result).toBe(false); }); - it('should return true when required validator exists without applyWhen rules', () => { - const element = { - validate: [{ type: 'required', value: true }], - } as unknown as IFormElement; - - const result = checkIfRequired(element, mockContext); - - expect(result).toBe(true); - }); - - it('should return true when validator with considerRequred exists without applyWhen rules', () => { - const element = { - validate: [{ type: 'someType', considerRequred: true }], - } as unknown as IFormElement; + it('should return true when there is a required validator with no conditions', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', + validate: [ + { + type: 'required', + value: {}, + message: 'Field is required', + }, + ], + }; - const result = checkIfRequired(element, mockContext); + const result = checkIfRequired(element, {}); expect(result).toBe(true); }); - it('should use executeRules when applyWhen rules exist', () => { - const element = { + it('should return true when there is a considerRequired validator with no conditions', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', validate: [ { - type: 'required', - applyWhen: [{ someRule: true }], + type: 'custom', + considerRequred: true, + value: {}, + message: 'Field is required', }, - ], - } as unknown as IFormElement; + ] as unknown as IFormElement['validate'], + }; - vi.mocked(executeRules).mockReturnValue([{ rule: {}, result: true }] as IRuleExecutionResult[]); + const result = checkIfRequired(element, {}); - const result = checkIfRequired(element, mockContext); - - expect(executeRules).toHaveBeenCalledWith(mockContext, [{ someRule: true }]); expect(result).toBe(true); }); - it('should return false when executeRules returns false', () => { - const element = { + it('should evaluate applyWhen conditions when present', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', validate: [ { type: 'required', - applyWhen: [{ someRule: true }], + value: {}, + message: 'Field is required', + applyWhen: { + type: 'json-logic', + value: { '==': [{ var: 'someField' }, true] }, + }, }, ], - } as unknown as IFormElement; + }; - vi.mocked(executeRules).mockReturnValue([ - { rule: {}, result: false }, - ] as IRuleExecutionResult[]); + const context = { someField: true }; - const result = checkIfRequired(element, mockContext); + mockedExecuteRules.mockReturnValue([{ result: true }] as IRuleExecutionResult[]); - expect(result).toBe(false); + const result = checkIfRequired(element, context); + + expect(result).toBe(true); + expect(mockedExecuteRules).toHaveBeenCalledWith(context, [ + { + engine: 'json-logic', + value: { '==': [{ var: 'someField' }, true] }, + }, + ]); }); - it('should return true if any required validator is applicable', () => { - const element = { + it('should return false when applyWhen condition evaluates to false', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', validate: [ { type: 'required', - applyWhen: [{ rule1: true }], - }, - { - type: 'required', - applyWhen: [{ rule2: true }], + value: {}, + message: 'Field is required', + applyWhen: { + type: 'json-logic', + value: { '==': [{ var: 'someField' }, true] }, + }, }, ], - } as unknown as IFormElement; + }; - vi.mocked(executeRules) - .mockReturnValueOnce([{ rule: {}, result: false }] as IRuleExecutionResult[]) - .mockReturnValueOnce([{ rule: {}, result: true }] as IRuleExecutionResult[]); + const context = { someField: false }; - const result = checkIfRequired(element, mockContext); + mockedExecuteRules.mockReturnValue([{ result: false, rule: {} }] as IRuleExecutionResult[]); - expect(result).toBe(true); + const result = checkIfRequired(element, context); + + expect(result).toBe(false); + expect(mockedExecuteRules).toHaveBeenCalledWith(context, [ + { + engine: 'json-logic', + value: { '==': [{ var: 'someField' }, true] }, + }, + ]); }); }); diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts index 51e7f976b7..1e4c5bd961 100644 --- a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts @@ -105,7 +105,7 @@ describe('useRuleEngine', () => { expect(result.current).toEqual([]); // Wait for custom delayed execution - await vi.advanceTimersByTimeAsync(customDelay); + await vi.advanceTimersByTimeAsync(1050); // Assert after delay expect(result.current).toEqual(expectedResults); From b633bcbe320c507d19e3cf526485b2d877ad0e57 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 20 Dec 2024 11:58:26 +0200 Subject: [PATCH 29/54] feat: added custom validators & custom inputs examples --- .../Form/DynamicForm/DynamicForm.stories.tsx | 10 +++ .../CustomInputsShowCase.tsx | 79 +++++++++++++++++++ .../_stories/CustomInputsShowcase/index.ts | 1 + .../CustomValidatorsShowcase.tsx | 57 +++++++++++++ .../CustomValidatorsShowcase/index.ts | 1 + 5 files changed, 148 insertions(+) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomInputsShowcase/CustomInputsShowCase.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomInputsShowcase/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomValidatorsShowcase/CustomValidatorsShowcase.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomValidatorsShowcase/index.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx index 62f763ac93..2fdff1d856 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx @@ -1,5 +1,7 @@ import { DynamicFormV2 } from './DynamicForm'; import { ConditionalRenderingShowcaseComponent } from './_stories/ConditionalRenderingShowcase'; +import { CustomInputsShowCaseComponent } from './_stories/CustomInputsShowcase'; +import { CustomValidatorsShowcaseComponent } from './_stories/CustomValidatorsShowcase'; import { FileUploadShowcaseComponent } from './_stories/FileUploadShowcase'; import { InputsShowcaseComponent } from './_stories/InputsShowcase'; import { ValidationShowcaseComponent } from './_stories/ValidationShowcase/ValidationShowcase'; @@ -23,3 +25,11 @@ export const ValidationShowcase = { export const ConditionalRenderingShowcase = { render: () => , }; + +export const CustomInputsShowCase = { + render: () => , +}; + +export const CustomValidatorsShowcase = { + render: () => , +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomInputsShowcase/CustomInputsShowCase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomInputsShowcase/CustomInputsShowCase.tsx new file mode 100644 index 0000000000..d6749adc3c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomInputsShowcase/CustomInputsShowCase.tsx @@ -0,0 +1,79 @@ +import { AnyObject } from '@/common'; +import { Input } from '@/components/atoms'; +import { useState } from 'react'; +import { JSONEditorComponent } from '../../../Validator/_stories/components/JsonEditor/JsonEditor'; +import { DynamicFormV2 } from '../../DynamicForm'; +import { useField } from '../../hooks/external'; +import { TDynamicFormField } from '../../types'; + +const CalculatorInput: TDynamicFormField = ({ element }) => { + const [values, setValues] = useState<{ input1: string; input2: string }>({ + input1: '', + input2: '', + }); + const { value, onChange } = useField(element); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value: inputValue } = e.target; + + setValues(prevValues => { + const newValues = { ...prevValues, [name]: inputValue }; + + const input1Value = Number(newValues.input1); + const input2Value = Number(newValues.input2); + + if (!isNaN(input1Value) && !isNaN(input2Value)) { + onChange(input1Value + input2Value); + } + + return newValues; + }); + }; + + return ( +
+

Calculator

+
+ + +
+
Sum: {value}
+
+ ); +}; + +const extendsFields = { + calculator: CalculatorInput, +}; + +const schema = [ + { + id: 'calculator', + element: 'calculator', + valueDestination: 'calculatorSum', + }, +]; + +export const CustomInputsShowCaseComponent = () => { + const [context, setContext] = useState({}); + + return ( +
+
+ { + console.log('onSubmit'); + }} + onChange={setContext} + fieldExtends={extendsFields} + // onEvent={console.log} + /> +
+
+ +
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomInputsShowcase/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomInputsShowcase/index.ts new file mode 100644 index 0000000000..f2eb3c7443 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomInputsShowcase/index.ts @@ -0,0 +1 @@ +export * from './CustomInputsShowCase'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomValidatorsShowcase/CustomValidatorsShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomValidatorsShowcase/CustomValidatorsShowcase.tsx new file mode 100644 index 0000000000..5d252f60d2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomValidatorsShowcase/CustomValidatorsShowcase.tsx @@ -0,0 +1,57 @@ +import { AnyObject } from '@/common'; +import { useState } from 'react'; +import { registerValidator, TValidator } from '../../../Validator'; +import { JSONEditorComponent } from '../../../Validator/_stories/components/JsonEditor/JsonEditor'; +import { DynamicFormV2 } from '../../DynamicForm'; +import { IFormElement } from '../../types'; + +const johnDoeCheckerValidator: TValidator = (value, context) => { + if (value !== 'John Doe') { + throw new Error('You has to be John Doe'); + } + + return true; +}; + +registerValidator('johnDoeChecker', johnDoeCheckerValidator); + +const schema: Array> = [ + { + id: 'johndoe', + element: 'textfield', + params: { + label: 'Full Name', + placeholder: 'Only John Doe allowed', + }, + valueDestination: 'fullName', + validate: [ + { + type: 'johnDoeChecker' as any, + value: {}, + }, + ], + }, +]; + +export const CustomValidatorsShowcaseComponent = () => { + const [context, setContext] = useState({}); + + return ( +
+
+ { + console.log('onSubmit'); + }} + onChange={setContext} + // onEvent={console.log} + /> +
+
+ +
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomValidatorsShowcase/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomValidatorsShowcase/index.ts new file mode 100644 index 0000000000..85cf70fba4 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/CustomValidatorsShowcase/index.ts @@ -0,0 +1 @@ +export * from './CustomValidatorsShowcase'; From 695e0bdff9a3dc66389f0fd1692d46166769093c Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 27 Dec 2024 12:41:17 +0200 Subject: [PATCH 30/54] feat: added radio field & tests --- .../InputsShowcase/InputsShowcase.tsx | 13 ++ .../fields/RadioField/RadioField.tsx | 52 ++++++++ .../RadioField/RadioField.unit.test.tsx | 117 ++++++++++++++++++ .../DynamicForm/fields/RadioField/index.ts | 1 + .../repositories/fields-repository.ts | 2 + 5 files changed, 185 insertions(+) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/index.ts 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 a118477ad7..7a0a1f4ee0 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 @@ -93,6 +93,19 @@ const schema: Array> = [ defaultCountry: 'il', }, }, + { + id: 'RadioField', + element: 'radiofield', + valueDestination: 'radio', + params: { + label: 'Radio Field', + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, + ], + }, + }, { id: 'FileField', element: 'filefield', diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx new file mode 100644 index 0000000000..63f179666b --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx @@ -0,0 +1,52 @@ +import { Label, RadioGroup } from '@/components/atoms'; +import { RadioGroupItem } from '@/components/atoms/RadioGroup/RadioGroup.Item'; +import { createTestId } from '@/components/organisms/Renderer'; +import { useField } from '../../hooks/external'; +import { FieldErrors } from '../../layouts/FieldErrors'; +import { FieldLayout } from '../../layouts/FieldLayout'; +import { ICommonFieldParams, TDynamicFormField } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; + +export interface IRadioFieldOption { + label: string; + value: any; +} + +export interface IRadioFieldParams extends ICommonFieldParams { + options: IRadioFieldOption[]; +} + +export const RadioField: TDynamicFormField = ({ element }) => { + const { stack } = useStack(); + const { value, onChange, onBlur, onFocus, disabled } = useField(element, stack); + const { options = [] } = element.params || {}; + + return ( + + + {options.map(({ value, label }) => ( +
+ + +
+ ))} +
+ +
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx new file mode 100644 index 0000000000..7f8f23ac79 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx @@ -0,0 +1,117 @@ +import { createTestId } from '@/components/organisms/Renderer'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useElement, useField } from '../../hooks/external'; +import { IFormElement } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; +import { IRadioFieldParams, RadioField } from './RadioField'; + +vi.mock('../../hooks/external', () => ({ + useField: vi.fn(), + useElement: vi.fn(), +})); + +vi.mock('../FieldList/providers/StackProvider', () => ({ + useStack: vi.fn(), +})); + +vi.mock('@/components/organisms/Renderer', () => ({ + createTestId: vi.fn(), +})); + +describe('RadioField', () => { + const mockElement = { + id: 'test-radio', + params: { + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ], + }, + } as IFormElement; + + const mockStack = { stack: [] }; + const mockFieldProps = { + value: '', + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + disabled: false, + touched: false, + }; + + beforeEach(() => { + vi.mocked(useStack).mockReturnValue(mockStack); + vi.mocked(useField).mockReturnValue(mockFieldProps); + vi.mocked(createTestId).mockImplementation(element => element.id); + vi.mocked(useElement).mockReturnValue({ + id: 'test-radio', + originId: 'test-radio', + hidden: false, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders radio options correctly', () => { + render(); + + mockElement.params?.options.forEach(option => { + expect(screen.getByLabelText(option.label)).toBeInTheDocument(); + }); + }); + + it('handles value change', async () => { + render(); + const user = userEvent.setup(); + + const radioOption = screen.getByLabelText('Option 1'); + await user.click(radioOption); + + expect(mockFieldProps.onChange).toHaveBeenCalled(); + }); + + it('handles blur event', async () => { + render(); + const user = userEvent.setup(); + + const radioGroup = screen.getByRole('radiogroup'); + await user.click(radioGroup); + await user.tab(); + + expect(mockFieldProps.onBlur).toHaveBeenCalled(); + }); + + it('handles focus event', async () => { + render(); + const user = userEvent.setup(); + + const radioOption = screen.getByLabelText('Option 1'); + await user.click(radioOption); + + expect(mockFieldProps.onFocus).toHaveBeenCalled(); + }); + + it('applies disabled state correctly', () => { + vi.mocked(useField).mockReturnValue({ + ...mockFieldProps, + disabled: true, + }); + + render(); + + mockElement.params?.options.forEach(option => { + expect(screen.getByLabelText(option.label)).toBeDisabled(); + }); + }); + + it('renders with correct test IDs', () => { + render(); + + expect(screen.getByTestId('test-radio-radio-group')).toBeInTheDocument(); + expect(screen.getAllByTestId('test-radio-radio-group-item')).toHaveLength(2); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/index.ts new file mode 100644 index 0000000000..39981f1165 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/index.ts @@ -0,0 +1 @@ +export * from './RadioField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts index 7466dc482d..c43b1e2bfb 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts @@ -7,6 +7,7 @@ import { FieldList } from '../fields/FieldList'; import { FileField } from '../fields/FileField'; import { MultiselectField } from '../fields/MultiselectField'; import { PhoneField } from '../fields/PhoneField'; +import { RadioField } from '../fields/RadioField'; import { SelectField } from '../fields/SelectField'; import { TextField } from '../fields/TextField'; import { TDynamicFormField } from '../types'; @@ -23,6 +24,7 @@ export const baseFields = { submitbutton: SubmitButton, phonefield: PhoneField, filefield: FileField, + radiofield: RadioField, } as const; export type TBaseFields = keyof typeof baseFields & string; From 13bb76ad40a3df71e8b389296f7d7aa589b91468 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 27 Dec 2024 13:39:57 +0200 Subject: [PATCH 31/54] feat: added tags input & tests & config fixes --- packages/ui/package.json | 2 + .../molecules/TagsInput/TagsInput.stories.tsx | 53 + .../molecules/TagsInput/TagsInput.tsx | 52 + .../components/molecules/TagsInput/index.ts | 1 + packages/ui/src/components/molecules/index.ts | 9 +- .../InputsShowcase/InputsShowcase.tsx | 8 + .../fields/TagsField/TagsField.tsx | 31 + .../fields/TagsField/TagsField.unit.test.tsx | 114 + .../DynamicForm/fields/TagsField/index.ts | 1 + .../repositories/fields-repository.ts | 2 + .../useRuleEngine/useRuleEngine.unit.test.ts | 2 +- packages/ui/tsconfig.json | 3 +- packages/ui/vite.config.ts | 4 + pnpm-lock.yaml | 1884 ++++++++++++++++- 14 files changed, 2073 insertions(+), 93 deletions(-) create mode 100644 packages/ui/src/components/molecules/TagsInput/TagsInput.stories.tsx create mode 100644 packages/ui/src/components/molecules/TagsInput/TagsInput.tsx create mode 100644 packages/ui/src/components/molecules/TagsInput/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/index.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index 6ca8b62b3c..e9bc04c391 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -56,12 +56,14 @@ "cmdk": "^0.2.0", "dayjs": "^1.11.6", "email-validator": "^2.0.4", + "emblor": "1.4.6", "i18n-iso-countries": "^7.6.0", "json-logic-js": "^2.0.2", "lodash": "^4.17.21", "lucide-react": "^0.144.0", "react": "^18.0.37", "react-dom": "^18.0.5", + "react-easy-sort": "^1.6.0", "react-error-boundary": "^4.0.13", "react-image": "^4.1.0", "react-json-view": "^1.21.3", diff --git a/packages/ui/src/components/molecules/TagsInput/TagsInput.stories.tsx b/packages/ui/src/components/molecules/TagsInput/TagsInput.stories.tsx new file mode 100644 index 0000000000..c033b530ab --- /dev/null +++ b/packages/ui/src/components/molecules/TagsInput/TagsInput.stories.tsx @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { TagsInput } from './TagsInput'; + +const meta = { + component: TagsInput, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + value: ['tag1', 'tag2', 'tag3'], + placeholder: 'Add tags...', + }, +}; + +export const Empty: Story = { + args: { + value: [], + placeholder: 'Add tags...', + }, +}; + +export const WithCustomStyles: Story = { + args: { + value: ['custom', 'styled', 'tags'], + placeholder: 'Add tags...', + styleClasses: { + container: 'border-2 border-blue-500 rounded-lg p-2', + tag: 'bg-blue-100 text-blue-800', + tagText: 'font-semibold', + removeButton: 'text-blue-500 hover:text-blue-700', + }, + }, +}; + +export const ReadOnly: Story = { + args: { + value: ['readonly', 'tags'], + placeholder: 'Add tags...', + readOnly: true, + }, +}; + +export const Disabled: Story = { + args: { + value: ['disabled', 'tags'], + placeholder: 'Add tags...', + disabled: true, + }, +}; diff --git a/packages/ui/src/components/molecules/TagsInput/TagsInput.tsx b/packages/ui/src/components/molecules/TagsInput/TagsInput.tsx new file mode 100644 index 0000000000..d50c502dd7 --- /dev/null +++ b/packages/ui/src/components/molecules/TagsInput/TagsInput.tsx @@ -0,0 +1,52 @@ +import { Tag, TagInput, TagInputProps } from 'emblor'; +import { FunctionComponent, useMemo, useState } from 'react'; + +export interface ITagsInputProps + extends Omit { + value?: string[]; + testId?: string; + onChange?: (tags: string[]) => void; + onBlur?: (event: React.FocusEvent) => void; + onFocus?: (event: React.FocusEvent) => void; +} + +export const TagsInput: FunctionComponent = ({ + value, + testId, + onChange, + onBlur, + onFocus, + ...props +}) => { + const [activeTagIndex, setActiveTagIndex] = useState(null); + + const tags = useMemo(() => { + if (!Array.isArray(value)) return []; + + return value.map((tag, index) => { + return { + id: String(index), + text: String(tag), + } satisfies Tag; + }); + }, [value]); + + return ( + onChange?.((tags as Tag[]).map(tag => tag.text))} + tags={tags} + activeTagIndex={activeTagIndex} + setActiveTagIndex={setActiveTagIndex} + addTagsOnBlur + styleClasses={{ + ...props.styleClasses, + input: + 'border-none outline-none focus:outline-none focus:ring-0 shadow-none placeholder:text-muted-foreground', + }} + /> + ); +}; diff --git a/packages/ui/src/components/molecules/TagsInput/index.ts b/packages/ui/src/components/molecules/TagsInput/index.ts new file mode 100644 index 0000000000..331b6c60cd --- /dev/null +++ b/packages/ui/src/components/molecules/TagsInput/index.ts @@ -0,0 +1 @@ +export * from './TagsInput'; diff --git a/packages/ui/src/components/molecules/index.ts b/packages/ui/src/components/molecules/index.ts index 1841c425ea..14e7e604b3 100644 --- a/packages/ui/src/components/molecules/index.ts +++ b/packages/ui/src/components/molecules/index.ts @@ -1,7 +1,8 @@ -export * from './JsonDialog'; -export * from './inputs'; -export * from './ErrorsList'; export * from './Accordion'; export * from './AccordionCard'; -export * from './RiskIndicatorsSummary'; +export * from './ErrorsList'; +export * from './inputs'; +export * from './JsonDialog'; export * from './RiskIndicator'; +export * from './RiskIndicatorsSummary'; +export * from './TagsInput'; 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 7a0a1f4ee0..dd43c34b59 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 @@ -106,6 +106,14 @@ const schema: Array> = [ ], }, }, + { + id: 'TagsField', + element: 'tagsfield', + valueDestination: 'tags', + params: { + label: 'Tags Field', + }, + }, { id: 'FileField', element: 'filefield', diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx new file mode 100644 index 0000000000..69ceb20e56 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx @@ -0,0 +1,31 @@ +import { TagsInput } from '@/components/molecules'; +import { createTestId } from '@/components/organisms/Renderer'; +import { useField } from '../../hooks/external'; +import { FieldErrors } from '../../layouts/FieldErrors'; +import { FieldLayout } from '../../layouts/FieldLayout'; +import { ICommonFieldParams, TDynamicFormField } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; + +export type ITextFieldParams = ICommonFieldParams; + +export const TagsField: TDynamicFormField = ({ element }) => { + const { stack } = useStack(); + const { value, onChange, onBlur, onFocus, disabled } = useField( + element, + stack, + ); + + return ( + + + + + ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx new file mode 100644 index 0000000000..55c660a213 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx @@ -0,0 +1,114 @@ +import { TagsInput } from '@/components/molecules'; +import { render } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { TDeepthLevelStack } from '../../../Validator'; +import { useElement, useField } from '../../hooks/external'; +import { IFormElement } from '../../types'; +import { useStack } from '../FieldList/providers/StackProvider'; +import { TagsField } from './TagsField'; + +vi.mock('@/components/molecules', () => ({ + TagsInput: vi.fn(props => ( + props.onChange?.(e.target.value.split(', '))} + onBlur={props.onBlur} + onFocus={props.onFocus} + disabled={props.disabled} + data-testid={props.testId} + /> + )), +})); + +vi.mock('../../hooks/external', () => ({ + useField: vi.fn(), + useElement: vi.fn(), +})); + +vi.mock('../FieldList/providers/StackProvider', () => ({ + useStack: vi.fn(), +})); + +describe('TagsField', () => { + const mockElement = { + id: 'test-tags', + element: 'tagsfield', + valueDestination: 'tags', + params: { + label: 'Test Tags', + }, + } as unknown as IFormElement; + + const mockStack = [] as unknown as TDeepthLevelStack; + const mockFieldProps = { + value: ['tag1', 'tag2'], + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + disabled: false, + touched: false, + }; + + beforeEach(() => { + vi.mocked(useStack).mockReturnValue({ stack: mockStack }); + vi.mocked(useField).mockReturnValue(mockFieldProps); + vi.mocked(useElement).mockReturnValue({ + id: 'test-tags', + originId: 'test-tags', + hidden: false, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders TagsInput with correct props', () => { + render(); + + expect(TagsInput).toHaveBeenCalledWith( + expect.objectContaining({ + value: mockFieldProps.value, + testId: `test-tags`, + onChange: mockFieldProps.onChange, + onBlur: mockFieldProps.onBlur, + onFocus: mockFieldProps.onFocus, + disabled: mockFieldProps.disabled, + }), + expect.anything(), + ); + }); + + it('passes undefined value correctly', () => { + vi.mocked(useField).mockReturnValue({ + ...mockFieldProps, + value: undefined, + }); + + render(); + + expect(TagsInput).toHaveBeenCalledWith( + expect.objectContaining({ + value: undefined, + }), + expect.anything(), + ); + }); + + it('handles disabled state', () => { + vi.mocked(useField).mockReturnValue({ + ...mockFieldProps, + disabled: true, + }); + + render(); + + expect(TagsInput).toHaveBeenCalledWith( + expect.objectContaining({ + disabled: true, + }), + expect.anything(), + ); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/index.ts new file mode 100644 index 0000000000..ba8821d39e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/index.ts @@ -0,0 +1 @@ +export * from './TagsField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts index c43b1e2bfb..09bb216015 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/repositories/fields-repository.ts @@ -9,6 +9,7 @@ import { MultiselectField } from '../fields/MultiselectField'; import { PhoneField } from '../fields/PhoneField'; import { RadioField } from '../fields/RadioField'; import { SelectField } from '../fields/SelectField'; +import { TagsField } from '../fields/TagsField'; import { TextField } from '../fields/TextField'; import { TDynamicFormField } from '../types'; @@ -25,6 +26,7 @@ export const baseFields = { phonefield: PhoneField, filefield: FileField, radiofield: RadioField, + tagsfield: TagsField, } as const; export type TBaseFields = keyof typeof baseFields & string; diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts index 1e4c5bd961..bf98d800cd 100644 --- a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts @@ -46,7 +46,7 @@ describe('useRuleEngine', () => { const { result } = renderHook(() => useRuleEngine(context, { rules, executeRulesSync: false })); // Wait for debounced execution - await vi.advanceTimersByTimeAsync(550); + await vi.advanceTimersByTimeAsync(600); // Assert expect(result.current).toEqual(expectedResults); diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 5f95b7a79c..afb7a18967 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -9,7 +9,8 @@ "@/*": ["./src/*"] }, "module": "ESNext", - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "esModuleInterop": true }, "include": ["src", "vitest.setup.ts"] } diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 6fa92905f1..77976d8ee8 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -52,6 +52,10 @@ export default defineConfig({ }, }, }, + // This needed for emblor and react-easy-sort to work during testing. + deps: { + inline: [/react-easy-sort/, /emblor/], + }, }, build: { outDir: 'dist', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89cc199d68..3ac4238560 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,7 +80,7 @@ importers: version: link:../../packages/common '@ballerine/react-pdf-toolkit': specifier: ^1.2.50 - version: link:../../packages/react-pdf-toolkit + version: 1.2.54(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.15)(@types/react@18.2.37)(date-fns@3.6.0)(react-dom@18.2.0)(react-pdf-tailwind@2.2.1)(react@18.2.0) '@ballerine/ui': specifier: ^0.5.50 version: link:../../packages/ui @@ -360,10 +360,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.27 - version: link:../../packages/config + version: 1.1.28 '@ballerine/eslint-config-react': specifier: ^2.0.27 - version: link:../../packages/eslint-config-react + version: 2.0.28(@ballerine/eslint-config@1.1.28)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -661,10 +661,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.27 - version: link:../../packages/config + version: 1.1.28 '@ballerine/eslint-config-react': specifier: ^2.0.27 - version: link:../../packages/eslint-config-react + version: 2.0.28(@ballerine/eslint-config@1.1.28)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2) '@jest/globals': specifier: ^29.7.0 version: 29.7.0 @@ -908,10 +908,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.27 - version: link:../../packages/config + version: 1.1.28 '@ballerine/eslint-config-react': specifier: ^2.0.27 - version: link:../../packages/eslint-config-react + version: 2.0.28(@ballerine/eslint-config@1.1.28)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -1098,7 +1098,7 @@ importers: dependencies: '@ballerine/react-pdf-toolkit': specifier: ^1.2.50 - version: link:../../packages/react-pdf-toolkit + version: 1.2.54(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react-pdf-tailwind@2.2.1)(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 @@ -1157,10 +1157,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.27 - version: link:../config + version: 1.1.28 '@ballerine/eslint-config': specifier: ^1.1.27 - version: link:../eslint-config + version: 1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@rollup/plugin-babel': specifier: 5.3.1 version: 5.3.1(@babel/core@7.17.9)(@types/babel__core@7.20.4)(rollup@2.70.2) @@ -1338,10 +1338,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.27 - version: link:../config + version: 1.1.28 '@ballerine/eslint-config': specifier: ^1.1.27 - version: link:../eslint-config + version: 1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -1490,7 +1490,7 @@ importers: dependencies: '@ballerine/eslint-config': specifier: ^1.1.27 - version: link:../eslint-config + version: 1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.56.0) eslint-plugin-react: specifier: ^7.33.2 version: 7.33.2(eslint@8.56.0) @@ -1502,7 +1502,7 @@ importers: dependencies: '@ballerine/config': specifier: ^1.1.27 - version: link:../config + version: 1.1.28 '@ballerine/ui': specifier: 0.5.50 version: link:../ui @@ -1624,10 +1624,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.27 - version: link:../config + version: 1.1.28 '@ballerine/eslint-config': specifier: ^1.1.27 - version: link:../eslint-config + version: 1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -1817,6 +1817,9 @@ importers: email-validator: specifier: ^2.0.4 version: 2.0.4 + emblor: + specifier: 1.4.6 + version: 1.4.6(@types/react-dom@18.2.15)(@types/react@18.2.37)(postcss@8.4.41)(react-dom@18.2.0)(react@18.2.0)(ts-node@10.9.1)(typescript@5.5.4) i18n-iso-countries: specifier: ^7.6.0 version: 7.7.0 @@ -1835,6 +1838,9 @@ importers: react-dom: specifier: ^18.0.5 version: 18.2.0(react@18.2.0) + react-easy-sort: + specifier: ^1.6.0 + version: 1.6.0(react-dom@18.2.0)(react@18.2.0) react-error-boundary: specifier: ^4.0.13 version: 4.0.13(react@18.2.0) @@ -1859,10 +1865,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.27 - version: link:../config + version: 1.1.28 '@ballerine/eslint-config-react': specifier: ^2.0.27 - version: link:../eslint-config-react + version: 2.0.28(@ballerine/eslint-config@1.1.28)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -2037,10 +2043,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.27 - version: link:../config + version: 1.1.28 '@ballerine/eslint-config': specifier: ^1.1.27 - version: link:../eslint-config + version: 1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -2327,10 +2333,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.27 - version: link:../../packages/config + version: 1.1.28 '@ballerine/eslint-config': specifier: ^1.1.27 - version: link:../../packages/eslint-config + version: 1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -2469,10 +2475,10 @@ importers: version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': specifier: ^1.1.27 - version: link:../../packages/config + version: 1.1.28 '@ballerine/eslint-config': specifier: ^1.1.27 - version: link:../../packages/eslint-config + version: 1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -2879,10 +2885,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.27 - version: link:../../packages/config + version: 1.1.28 '@ballerine/eslint-config': specifier: ^1.1.27 - version: link:../../packages/eslint-config + version: 1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@8.10.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0) '@cspell/cspell-types': specifier: ^6.31.1 version: 6.31.3 @@ -3057,10 +3063,10 @@ importers: devDependencies: '@ballerine/config': specifier: ^1.1.27 - version: link:../../packages/config + version: 1.1.28 '@ballerine/eslint-config': specifier: ^1.1.27 - version: link:../../packages/eslint-config + version: 1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/parser@6.14.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0) eslint: specifier: ^8.46.0 version: 8.54.0 @@ -3069,7 +3075,7 @@ importers: version: 9.0.0(eslint@8.54.0) eslint-config-standard-with-typescript: specifier: ^37.0.0 - version: 37.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.54.0)(typescript@5.5.4) + version: 37.0.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.54.0)(typescript@5.5.4) eslint-plugin-astro: specifier: ^0.28.0 version: 0.28.0(eslint@8.54.0) @@ -5029,7 +5035,7 @@ packages: '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.6 + debug: 4.4.0 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -7571,7 +7577,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.6 '@babel/types': 7.23.6 - debug: 4.3.6 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7586,7 +7592,7 @@ packages: '@babel/parser': 7.25.6 '@babel/template': 7.25.0 '@babel/types': 7.25.6 - debug: 4.3.6 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7628,6 +7634,313 @@ packages: resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} dev: true + /@ballerine/common@0.9.60: + resolution: {integrity: sha512-eFxu70h4b4JgU/P/AyuDZ7HsMFVHCLtWd3aGGENvyVpUS5gQRxocP8Qibd7BZB1iOhWaURVI7Clz3XOGciriyg==} + engines: {node: '>=12'} + dependencies: + '@sinclair/typebox': 0.32.15 + ajv: 8.13.0 + crypto-js: 4.2.0 + dayjs: 1.11.10 + json-schema-to-zod: 0.6.3 + lodash.get: 4.4.2 + lodash.isempty: 4.4.0 + xstate: 5.18.2 + zod: 3.23.4 + dev: false + + /@ballerine/config@1.1.28: + resolution: {integrity: sha512-ffwd985w2U1vq4aVr7ouhIrNbIzJ2UWbE6JEFwWJv0QmP+5wTPIBYDO/ShxayBZsDFwuYnrWv0LilaSStbqhLQ==} + + /@ballerine/eslint-config-react@2.0.28(@ballerine/eslint-config@1.1.28)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2): + resolution: {integrity: sha512-7Ls6bwM0nQlJa0kKfVb7IO/kaHGZZP4y0HuXlE6onon+VgjNIDPCeX2elPdOUG9920SDj56XVJBYnxAP2l6ETw==} + peerDependencies: + '@ballerine/eslint-config': ^1.1.28 + eslint-plugin-react: ^7.33.2 + eslint-plugin-react-hooks: ^4.6.0 + dependencies: + '@ballerine/eslint-config': 1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0) + eslint-plugin-react: 7.33.2(eslint@8.54.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.54.0) + dev: true + + /@ballerine/eslint-config@1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@6.15.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@2.0.0)(eslint@8.54.0): + resolution: {integrity: sha512-a8o5j9vpvjblxwu0EHAv6WSOFnVy8F1HGvnb/xDEZrl0k12wh2Nr6FKLcv6Looktfvh1FQXNao7L2vwrRuo+qA==} + peerDependencies: + '@stylistic/eslint-plugin-ts': ^1.6.2 + '@typescript-eslint/eslint-plugin': ^6.11.0 + '@typescript-eslint/parser': ^6.11.0 + eslint: ^8.53.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prefer-arrow: ^1.2.3 + eslint-plugin-unused-imports: ^3.0.0 + dependencies: + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@5.1.6) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.1.6) + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.1.6) + eslint: 8.54.0 + eslint-config-prettier: 6.15.0(eslint@8.54.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.54.0) + eslint-plugin-unused-imports: 2.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) + dev: true + + /@ballerine/eslint-config@1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@8.10.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0): + resolution: {integrity: sha512-a8o5j9vpvjblxwu0EHAv6WSOFnVy8F1HGvnb/xDEZrl0k12wh2Nr6FKLcv6Looktfvh1FQXNao7L2vwrRuo+qA==} + peerDependencies: + '@stylistic/eslint-plugin-ts': ^1.6.2 + '@typescript-eslint/eslint-plugin': ^6.11.0 + '@typescript-eslint/parser': ^6.11.0 + eslint: ^8.53.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prefer-arrow: ^1.2.3 + eslint-plugin-unused-imports: ^3.0.0 + dependencies: + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@4.9.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@4.9.3) + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@4.9.3) + eslint: 8.54.0 + eslint-config-prettier: 8.10.0(eslint@8.54.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.54.0) + eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) + dev: true + + /@ballerine/eslint-config@1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0): + resolution: {integrity: sha512-a8o5j9vpvjblxwu0EHAv6WSOFnVy8F1HGvnb/xDEZrl0k12wh2Nr6FKLcv6Looktfvh1FQXNao7L2vwrRuo+qA==} + peerDependencies: + '@stylistic/eslint-plugin-ts': ^1.6.2 + '@typescript-eslint/eslint-plugin': ^6.11.0 + '@typescript-eslint/parser': ^6.11.0 + eslint: ^8.53.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prefer-arrow: ^1.2.3 + eslint-plugin-unused-imports: ^3.0.0 + dependencies: + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.5.4) + eslint: 8.54.0 + eslint-config-prettier: 9.0.0(eslint@8.54.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.54.0) + eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) + dev: true + + /@ballerine/eslint-config@1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.56.0): + resolution: {integrity: sha512-a8o5j9vpvjblxwu0EHAv6WSOFnVy8F1HGvnb/xDEZrl0k12wh2Nr6FKLcv6Looktfvh1FQXNao7L2vwrRuo+qA==} + peerDependencies: + '@stylistic/eslint-plugin-ts': ^1.6.2 + '@typescript-eslint/eslint-plugin': ^6.11.0 + '@typescript-eslint/parser': ^6.11.0 + eslint: ^8.53.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prefer-arrow: ^1.2.3 + eslint-plugin-unused-imports: ^3.0.0 + dependencies: + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.56.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.5.4) + eslint: 8.56.0 + eslint-config-prettier: 9.0.0(eslint@8.56.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.56.0) + eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@6.14.0)(eslint@8.56.0) + dev: false + + /@ballerine/eslint-config@1.1.28(@stylistic/eslint-plugin-ts@1.6.2)(@typescript-eslint/parser@6.14.0)(eslint-config-prettier@9.0.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-unused-imports@3.0.0)(eslint@8.54.0): + resolution: {integrity: sha512-a8o5j9vpvjblxwu0EHAv6WSOFnVy8F1HGvnb/xDEZrl0k12wh2Nr6FKLcv6Looktfvh1FQXNao7L2vwrRuo+qA==} + peerDependencies: + '@stylistic/eslint-plugin-ts': ^1.6.2 + '@typescript-eslint/eslint-plugin': ^6.11.0 + '@typescript-eslint/parser': ^6.11.0 + eslint: ^8.53.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prefer-arrow: ^1.2.3 + eslint-plugin-unused-imports: ^3.0.0 + dependencies: + '@stylistic/eslint-plugin-ts': 1.6.2(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.14.0(eslint@8.54.0)(typescript@5.5.4) + eslint: 8.54.0 + eslint-config-prettier: 9.0.0(eslint@8.54.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.54.0) + eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.54.0) + dev: true + + /@ballerine/react-pdf-toolkit@1.2.54(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.15)(@types/react@18.2.37)(date-fns@3.6.0)(react-dom@18.2.0)(react-pdf-tailwind@2.2.1)(react@18.2.0): + resolution: {integrity: sha512-jzVk49/5HBX2vHkYqAPKNJnw3fpggIsE8onODiTua4ccrbN1Lu2080RCOx5kL+xFtSTPhmCgO0vkvJ7cocT1HQ==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + react-pdf-tailwind: ^2.2.1 + dependencies: + '@ballerine/config': 1.1.28 + '@ballerine/ui': 0.5.54(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.15)(@types/react@18.2.37)(date-fns@3.6.0) + '@react-pdf/renderer': 3.1.14(react@18.2.0) + '@sinclair/typebox': 0.31.26 + ajv: 8.13.0 + ajv-formats: 2.1.1(ajv@8.13.0) + class-variance-authority: 0.7.1 + dayjs: 1.11.10 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-pdf-tailwind: 2.2.1(react@18.2.0)(ts-node@10.9.1) + string-ts: 1.3.3 + transitivePeerDependencies: + - '@babel/runtime' + - '@mui/system' + - '@types/react' + - '@types/react-dom' + - date-fns + - date-fns-jalali + - encoding + - luxon + - moment + - moment-hijri + - moment-jalaali + - supports-color + dev: false + + /@ballerine/react-pdf-toolkit@1.2.54(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react-pdf-tailwind@2.2.1)(react@18.2.0): + resolution: {integrity: sha512-jzVk49/5HBX2vHkYqAPKNJnw3fpggIsE8onODiTua4ccrbN1Lu2080RCOx5kL+xFtSTPhmCgO0vkvJ7cocT1HQ==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + react-pdf-tailwind: ^2.2.1 + dependencies: + '@ballerine/config': 1.1.28 + '@ballerine/ui': 0.5.54(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.17)(@types/react@18.2.43) + '@react-pdf/renderer': 3.1.14(react@18.2.0) + '@sinclair/typebox': 0.31.26 + ajv: 8.13.0 + ajv-formats: 2.1.1(ajv@8.13.0) + class-variance-authority: 0.7.1 + dayjs: 1.11.10 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-pdf-tailwind: 2.2.1(react@18.2.0)(ts-node@10.9.1) + string-ts: 1.3.3 + transitivePeerDependencies: + - '@babel/runtime' + - '@mui/system' + - '@types/react' + - '@types/react-dom' + - date-fns + - date-fns-jalali + - encoding + - luxon + - moment + - moment-hijri + - moment-jalaali + - supports-color + dev: false + + /@ballerine/ui@0.5.54(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.15)(@types/react@18.2.37)(date-fns@3.6.0): + resolution: {integrity: sha512-i3Fj6wiRi4gjujc7ZDETFE8LDFfaTMNUcib2jZRZ1z/3lHhuKuzNeQYEQums47WDSAxoGeQtPPuCilOE/O8/Mg==} + dependencies: + '@ballerine/common': 0.9.60 + '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) + '@mui/material': 5.14.18(@emotion/react@11.11.1)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-date-pickers': 6.18.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.18)(@mui/system@5.15.6)(@types/react@18.2.37)(date-fns@3.6.0)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-accordion': 1.1.2(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-checkbox': 1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-hover-card': 1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-icons': 1.3.0(react@18.2.0) + '@radix-ui/react-label': 2.0.2(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popover': 1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-radio-group': 1.1.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-scroll-area': 1.0.5(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.37)(react@18.2.0) + '@rjsf/core': 5.14.2(@rjsf/utils@5.14.2)(react@18.2.0) + '@rjsf/utils': 5.14.2(react@18.2.0) + '@rjsf/validator-ajv8': 5.14.2(@rjsf/utils@5.14.2) + '@tanstack/react-table': 8.10.7(react-dom@18.2.0)(react@18.2.0) + class-variance-authority: 0.6.1 + clsx: 1.2.1 + cmdk: 0.2.0(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + dayjs: 1.11.10 + i18n-iso-countries: 7.7.0 + lodash: 4.17.21 + lucide-react: 0.144.0(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-error-boundary: 4.0.13(react@18.2.0) + react-image: 4.1.0(@babel/runtime@7.23.8)(react-dom@18.2.0)(react@18.2.0) + react-json-view: 1.21.3(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + react-phone-input-2: 2.15.1(react-dom@18.2.0)(react@18.2.0) + string-ts: 1.3.3 + tailwind-merge: 1.14.0 + zod: 3.23.4 + transitivePeerDependencies: + - '@babel/runtime' + - '@mui/system' + - '@types/react' + - '@types/react-dom' + - date-fns + - date-fns-jalali + - encoding + - luxon + - moment + - moment-hijri + - moment-jalaali + - supports-color + dev: false + + /@ballerine/ui@0.5.54(@babel/runtime@7.23.8)(@mui/system@5.15.6)(@types/react-dom@18.2.17)(@types/react@18.2.43): + resolution: {integrity: sha512-i3Fj6wiRi4gjujc7ZDETFE8LDFfaTMNUcib2jZRZ1z/3lHhuKuzNeQYEQums47WDSAxoGeQtPPuCilOE/O8/Mg==} + dependencies: + '@ballerine/common': 0.9.60 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0) + '@mui/material': 5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-date-pickers': 6.18.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.18)(@mui/system@5.15.6)(@types/react@18.2.43)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-accordion': 1.1.2(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-checkbox': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-hover-card': 1.0.7(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-icons': 1.3.0(react@18.2.0) + '@radix-ui/react-label': 2.0.2(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popover': 1.0.7(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-radio-group': 1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-scroll-area': 1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.43)(react@18.2.0) + '@rjsf/core': 5.14.2(@rjsf/utils@5.14.2)(react@18.2.0) + '@rjsf/utils': 5.14.2(react@18.2.0) + '@rjsf/validator-ajv8': 5.14.2(@rjsf/utils@5.14.2) + '@tanstack/react-table': 8.10.7(react-dom@18.2.0)(react@18.2.0) + class-variance-authority: 0.6.1 + clsx: 1.2.1 + cmdk: 0.2.0(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + dayjs: 1.11.10 + i18n-iso-countries: 7.7.0 + lodash: 4.17.21 + lucide-react: 0.144.0(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-error-boundary: 4.0.13(react@18.2.0) + react-image: 4.1.0(@babel/runtime@7.23.8)(react-dom@18.2.0)(react@18.2.0) + react-json-view: 1.21.3(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + react-phone-input-2: 2.15.1(react-dom@18.2.0)(react@18.2.0) + string-ts: 1.3.3 + tailwind-merge: 1.14.0 + zod: 3.23.4 + transitivePeerDependencies: + - '@babel/runtime' + - '@mui/system' + - '@types/react' + - '@types/react-dom' + - date-fns + - date-fns-jalali + - encoding + - luxon + - moment + - moment-hijri + - moment-jalaali + - supports-color + dev: false + /@base2/pretty-print-object@1.0.1: resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} dev: true @@ -8593,6 +8906,29 @@ packages: - supports-color dev: false + /@emotion/react@11.11.1(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/babel-plugin': 11.11.0 + '@emotion/cache': 11.11.0 + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + '@types/react': 18.2.43 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + transitivePeerDependencies: + - supports-color + dev: false + /@emotion/serialize@1.1.2: resolution: {integrity: sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==} dependencies: @@ -8630,6 +8966,29 @@ packages: - supports-color dev: false + /@emotion/styled@11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/babel-plugin': 11.11.0 + '@emotion/is-prop-valid': 1.2.1 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@types/react': 18.2.43 + react: 18.2.0 + transitivePeerDependencies: + - supports-color + dev: false + /@emotion/unitless@0.8.1: resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} dev: false @@ -10791,6 +11150,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@mui/base@5.0.0-beta.24(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bKt2pUADHGQtqWDZ8nvL2Lvg2GNJyd/ZUgZAJoYzRgmnxBL9j36MSlS3+exEdYkikcnvVafcBtD904RypFKb0w==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@floating-ui/react-dom': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.43) + '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react': 18.2.43 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@mui/core-downloads-tracker@5.14.18: resolution: {integrity: sha512-yFpF35fEVDV81nVktu0BE9qn2dD/chs7PsQhlyaV3EnTeZi9RZBuvoEfRym1/jmhJ2tcfeWXiRuHG942mQXJJQ==} dev: false @@ -10831,6 +11213,77 @@ packages: react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) dev: false + /@mui/material@5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-y3UiR/JqrkF5xZR0sIKj6y7xwuEiweh9peiN3Zfjy1gXWXhz5wjlaLdoxFfKIEBUFfeQALxr/Y8avlHH+B9lpQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.14.18 + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.43) + '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-transition-group': 4.4.9 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + dev: false + + /@mui/material@5.14.18(@emotion/react@11.11.1)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-y3UiR/JqrkF5xZR0sIKj6y7xwuEiweh9peiN3Zfjy1gXWXhz5wjlaLdoxFfKIEBUFfeQALxr/Y8avlHH+B9lpQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.14.18 + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.37) + '@mui/utils': 5.15.6(@types/react@18.2.37)(react@18.2.0) + '@types/react': 18.2.37 + '@types/react-transition-group': 4.4.9 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + dev: false + /@mui/private-theming@5.15.6(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-ZBX9E6VNUSscUOtU8uU462VvpvBS7eFl5VfxAzTRVQBHflzL+5KtnGrebgf6Nd6cdvxa1o0OomiaxSKoN2XDmg==} engines: {node: '>=12.0.0'} @@ -10848,6 +11301,23 @@ packages: react: 18.2.0 dev: false + /@mui/private-theming@5.15.6(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-ZBX9E6VNUSscUOtU8uU462VvpvBS7eFl5VfxAzTRVQBHflzL+5KtnGrebgf6Nd6cdvxa1o0OomiaxSKoN2XDmg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@mui/styled-engine@5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): resolution: {integrity: sha512-KAn8P8xP/WigFKMlEYUpU9z2o7jJnv0BG28Qu1dhNQVutsLVIFdRf5Nb+0ijp2qgtcmygQ0FtfRuXv5LYetZTg==} engines: {node: '>=12.0.0'} @@ -10900,6 +11370,36 @@ packages: react: 18.2.0 dev: false + /@mui/system@5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-J01D//u8IfXvaEHMBQX5aO2l7Q+P15nt96c4NskX7yp5/+UuZP8XCQJhtBtLuj+M2LLyXHYGmCPeblsmmscP2Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0) + '@mui/private-theming': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@mui/styled-engine': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.43) + '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@mui/types@7.2.13(@types/react@18.2.37): resolution: {integrity: sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==} peerDependencies: @@ -10911,6 +11411,17 @@ packages: '@types/react': 18.2.37 dev: false + /@mui/types@7.2.13(@types/react@18.2.43): + resolution: {integrity: sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.43 + dev: false + /@mui/utils@5.15.6(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-qfEhf+zfU9aQdbzo1qrSWlbPQhH1nCgeYgwhOVnj9Bn39shJQitEnXpSQpSNag8+uty5Od6PxmlNKPTnPySRKA==} engines: {node: '>=12.0.0'} @@ -10929,6 +11440,80 @@ packages: react-is: 18.2.0 dev: false + /@mui/utils@5.15.6(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-qfEhf+zfU9aQdbzo1qrSWlbPQhH1nCgeYgwhOVnj9Bn39shJQitEnXpSQpSNag8+uty5Od6PxmlNKPTnPySRKA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@types/prop-types': 15.7.11 + '@types/react': 18.2.43 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + + /@mui/x-date-pickers@6.18.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.18)(@mui/system@5.15.6)(@types/react@18.2.37)(date-fns@3.6.0)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-22gWCzejBGG4Kycpk/yYhzV6Pj/xVBvaz5A1rQmCD/0DCXTDJvXPG8qvzrNlp1wj8q+rAQO82e/+conUGgYgYg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.14.18(@emotion/react@11.11.1)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) + '@mui/utils': 5.15.6(@types/react@18.2.37)(react@18.2.0) + '@types/react-transition-group': 4.4.9 + clsx: 2.1.0 + date-fns: 3.6.0 + dayjs: 1.11.10 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /@mui/x-date-pickers@6.18.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.18)(@mui/system@5.15.6)(@types/react@18.2.37)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-22gWCzejBGG4Kycpk/yYhzV6Pj/xVBvaz5A1rQmCD/0DCXTDJvXPG8qvzrNlp1wj8q+rAQO82e/+conUGgYgYg==} engines: {node: '>=14.0.0'} @@ -10984,6 +11569,61 @@ packages: - '@types/react' dev: false + /@mui/x-date-pickers@6.18.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.18)(@mui/system@5.15.6)(@types/react@18.2.43)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-22gWCzejBGG4Kycpk/yYhzV6Pj/xVBvaz5A1rQmCD/0DCXTDJvXPG8qvzrNlp1wj8q+rAQO82e/+conUGgYgYg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@emotion/react': 11.11.1(@types/react@18.2.43)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.43)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.6(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.43)(react@18.2.0) + '@mui/utils': 5.15.6(@types/react@18.2.43)(react@18.2.0) + '@types/react-transition-group': 4.4.9 + clsx: 2.1.0 + dayjs: 1.11.10 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /@ndelangen/get-tarball@3.0.9: resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==} dependencies: @@ -11664,6 +12304,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-accordion@1.1.2(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-arrow@1.0.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==} peerDependencies: @@ -11715,7 +12384,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-aspect-ratio@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-fXR5kbMan9oQqMuacfzlGG/SQMcmMlZ4wrvpckv8SgUulD0MMpspxJrxg/Gp/ISV3JfV1AeSWTYK9GvxA4ySwA==} @@ -11790,6 +12458,34 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} peerDependencies: @@ -11818,6 +12514,34 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} peerDependencies: @@ -11863,7 +12587,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-collection@1.1.0(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} @@ -11922,7 +12645,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} @@ -11937,6 +12659,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.43 + react: 18.2.0 + dev: false + /@radix-ui/react-context@1.0.0(react@18.2.0): resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==} peerDependencies: @@ -11971,7 +12706,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-context@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} @@ -12013,6 +12747,33 @@ packages: - '@types/react' dev: false + /@radix-ui/react-dialog@1.0.0(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.0 + '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) + '@radix-ui/react-context': 1.0.0(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.0(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.0(react@18.2.0) + '@radix-ui/react-portal': 1.0.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.0(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0) + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.4(@types/react@18.2.43)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /@radix-ui/react-dialog@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==} peerDependencies: @@ -12047,6 +12808,40 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.37)(react@18.2.0) dev: false + /@radix-ui/react-dialog@1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.43)(react@18.2.0) + dev: false + /@radix-ui/react-direction@1.0.1(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: @@ -12072,7 +12867,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-direction@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} @@ -12166,7 +12960,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} @@ -12193,6 +12986,31 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} peerDependencies: @@ -12220,6 +13038,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.0(react@18.2.0): resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==} peerDependencies: @@ -12254,7 +13099,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-focus-scope@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==} @@ -12313,7 +13157,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} @@ -12338,6 +13181,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-hover-card@1.0.2(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-LOqJAHdjjLoIhOCHdFO5ASkNACG/wwPQljzrm4U53n1Uxa1Crheazs82dST1946zgu4p0U4IrFmuQ6PTODIlkw==} peerDependencies: @@ -12389,6 +13255,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-hover-card@1.0.7(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-icons@1.3.0(react@18.2.0): resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: @@ -12434,7 +13329,6 @@ packages: '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-id@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} @@ -12471,6 +13365,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} peerDependencies: @@ -12509,6 +13424,44 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.37)(react@18.2.0) dev: false + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.43)(react@18.2.0) + dev: false + /@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==} peerDependencies: @@ -12544,6 +13497,41 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.37)(react@18.2.0) dev: false + /@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.43)(react@18.2.0) + dev: false + /@radix-ui/react-popper@1.0.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-J4Vj7k3k+EHNWgcKrE+BLlQfpewxA7Zd76h5I0bIa+/EqaIZ3DuwrbPj49O3wqN+STnXsBuxiHLiF0iU3yfovw==} peerDependencies: @@ -12655,6 +13643,36 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@floating-ui/react-dom': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/rect': 1.0.1 + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-portal@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==} peerDependencies: @@ -12718,7 +13736,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} @@ -12741,6 +13758,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==} peerDependencies: @@ -12776,6 +13814,28 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-primitive@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==} peerDependencies: @@ -12839,7 +13899,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} @@ -12891,6 +13950,36 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-radio-group@1.1.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.8 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -12946,7 +14035,6 @@ packages: '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} @@ -13005,6 +14093,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-scroll-area@1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-select@1.2.2(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} peerDependencies: @@ -13214,7 +14331,6 @@ packages: '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-slot@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} @@ -13230,6 +14346,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-slot@1.1.0(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + react: 18.2.0 + dev: false + /@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==} peerDependencies: @@ -13605,7 +14735,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} @@ -13657,7 +14786,6 @@ packages: '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} @@ -13720,7 +14848,6 @@ packages: '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-layout-effect@1.0.0(react@18.2.0): resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==} @@ -13756,7 +14883,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} @@ -13796,7 +14922,6 @@ packages: '@babel/runtime': 7.23.8 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-rect@1.0.0(react@18.2.0): resolution: {integrity: sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==} @@ -13835,7 +14960,6 @@ packages: '@radix-ui/rect': 1.0.1 '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-use-size@1.0.0(react@18.2.0): resolution: {integrity: sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==} @@ -13874,7 +14998,6 @@ packages: '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@types/react': 18.2.43 react: 18.2.0 - dev: true /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} @@ -17335,6 +18458,34 @@ packages: espree: 9.6.1 dev: false + /@stylistic/eslint-plugin-js@1.6.2(eslint@8.54.0): + resolution: {integrity: sha512-ndT6X2KgWGxv8101pdMOxL8pihlYIHcOv3ICd70cgaJ9exwkPn8hJj4YQwslxoAlre1TFHnXd/G1/hYXgDrjIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@types/eslint': 8.56.5 + acorn: 8.11.3 + escape-string-regexp: 4.0.0 + eslint: 8.54.0 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + dev: true + + /@stylistic/eslint-plugin-js@1.6.2(eslint@8.56.0): + resolution: {integrity: sha512-ndT6X2KgWGxv8101pdMOxL8pihlYIHcOv3ICd70cgaJ9exwkPn8hJj4YQwslxoAlre1TFHnXd/G1/hYXgDrjIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@types/eslint': 8.56.5 + acorn: 8.11.3 + escape-string-regexp: 4.0.0 + eslint: 8.56.0 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + dev: false + /@stylistic/eslint-plugin-ts@1.6.2(eslint@8.53.0)(typescript@5.5.4): resolution: {integrity: sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -17350,6 +18501,66 @@ packages: - typescript dev: false + /@stylistic/eslint-plugin-ts@1.6.2(eslint@8.54.0)(typescript@4.9.3): + resolution: {integrity: sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@stylistic/eslint-plugin-js': 1.6.2(eslint@8.54.0) + '@types/eslint': 8.56.5 + '@typescript-eslint/utils': 6.21.0(eslint@8.54.0)(typescript@4.9.3) + eslint: 8.54.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@stylistic/eslint-plugin-ts@1.6.2(eslint@8.54.0)(typescript@5.1.6): + resolution: {integrity: sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@stylistic/eslint-plugin-js': 1.6.2(eslint@8.54.0) + '@types/eslint': 8.56.5 + '@typescript-eslint/utils': 6.21.0(eslint@8.54.0)(typescript@5.1.6) + eslint: 8.54.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@stylistic/eslint-plugin-ts@1.6.2(eslint@8.54.0)(typescript@5.5.4): + resolution: {integrity: sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@stylistic/eslint-plugin-js': 1.6.2(eslint@8.54.0) + '@types/eslint': 8.56.5 + '@typescript-eslint/utils': 6.21.0(eslint@8.54.0)(typescript@5.5.4) + eslint: 8.54.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@stylistic/eslint-plugin-ts@1.6.2(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: '>=8.40.0' + dependencies: + '@stylistic/eslint-plugin-js': 1.6.2(eslint@8.56.0) + '@types/eslint': 8.56.5 + '@typescript-eslint/utils': 6.21.0(eslint@8.56.0)(typescript@5.5.4) + eslint: 8.56.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.5.2)(svelte@3.59.2)(vite@4.5.3): resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==} engines: {node: ^14.18.0 || >= 16} @@ -18719,7 +19930,6 @@ packages: dependencies: '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 - dev: false /@types/estree-jsx@1.0.3: resolution: {integrity: sha512-pvQ+TKeRHeiUGRhvYwRrQ/ISnohKkSJR14fT2yqyZ4e9K5vqc7hrtY2Y1Dw0ZwAzQ6DQsxsaCUuSIIi8v0Cq6w==} @@ -19186,7 +20396,6 @@ packages: resolution: {integrity: sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==} dependencies: '@types/react': 18.2.43 - dev: true /@types/react-helmet@6.1.9: resolution: {integrity: sha512-nuOeTefP4yPTWHvjGksCBKb/4hsgJxSX7aSTjTIDFXJIkZ6Wo2Y4/cmE1FO9OlYBrHjKOer/0zLwY7s4qiQBtw==} @@ -19213,7 +20422,6 @@ packages: '@types/prop-types': 15.7.10 '@types/scheduler': 0.16.6 csstype: 3.1.2 - dev: true /@types/react@18.2.46: resolution: {integrity: sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w==} @@ -19654,6 +20862,35 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/type-utils': 6.14.0(eslint@8.56.0)(typescript@5.5.4) + '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.3.0 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/experimental-utils@4.33.0(eslint@8.54.0)(typescript@4.9.5): resolution: {integrity: sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==} engines: {node: ^10.12.0 || >=12.0.0} @@ -19831,6 +21068,27 @@ packages: - supports-color dev: false + /@typescript-eslint/parser@6.14.0(eslint@8.54.0)(typescript@5.5.4): + resolution: {integrity: sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.54.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@6.14.0(eslint@8.55.0)(typescript@5.2.2): resolution: {integrity: sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -19852,6 +21110,27 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@6.14.0(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.56.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/scope-manager@4.33.0: resolution: {integrity: sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==} engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} @@ -19882,7 +21161,6 @@ packages: dependencies: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/visitor-keys': 6.14.0 - dev: true /@typescript-eslint/scope-manager@6.21.0: resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} @@ -19890,7 +21168,6 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - dev: false /@typescript-eslint/type-utils@5.62.0(eslint@8.22.0)(typescript@4.9.5): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} @@ -20052,6 +21329,26 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils@6.14.0(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.5.4) + '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.5.4) + debug: 4.3.6 + eslint: 8.56.0 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/types@4.33.0: resolution: {integrity: sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==} engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} @@ -20070,12 +21367,10 @@ packages: /@typescript-eslint/types@6.14.0: resolution: {integrity: sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==} engines: {node: ^16.0.0 || >=18.0.0} - dev: true /@typescript-eslint/types@6.21.0: resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} - dev: false /@typescript-eslint/typescript-estree@4.33.0(typescript@4.9.5): resolution: {integrity: sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==} @@ -20245,6 +21540,70 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@6.14.0(typescript@5.5.4): + resolution: {integrity: sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.6 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + /@typescript-eslint/typescript-estree@6.21.0(typescript@4.9.3): + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.6 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@4.9.3) + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.1.6): + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.6 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.5.4): resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -20265,7 +21624,6 @@ packages: typescript: 5.5.4 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/utils@5.62.0(eslint@8.22.0)(typescript@4.9.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} @@ -20425,6 +21783,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@6.14.0(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.5.4) + eslint: 8.56.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /@typescript-eslint/utils@6.21.0(eslint@8.53.0)(typescript@5.5.4): resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -20444,6 +21821,82 @@ packages: - typescript dev: false + /@typescript-eslint/utils@6.21.0(eslint@8.54.0)(typescript@4.9.3): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@4.9.3) + eslint: 8.54.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@6.21.0(eslint@8.54.0)(typescript@5.1.6): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) + eslint: 8.54.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@6.21.0(eslint@8.54.0)(typescript@5.5.4): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) + eslint: 8.54.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@6.21.0(eslint@8.56.0)(typescript@5.5.4): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) + eslint: 8.56.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /@typescript-eslint/visitor-keys@4.33.0: resolution: {integrity: sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==} engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} @@ -20474,7 +21927,6 @@ packages: dependencies: '@typescript-eslint/types': 6.14.0 eslint-visitor-keys: 3.4.3 - dev: true /@typescript-eslint/visitor-keys@6.21.0: resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} @@ -20482,7 +21934,6 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 eslint-visitor-keys: 3.4.3 - dev: false /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -21376,7 +22827,7 @@ packages: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} dependencies: - debug: 4.3.6 + debug: 4.4.0 transitivePeerDependencies: - supports-color dev: true @@ -23072,6 +24523,20 @@ packages: - '@types/react' dev: false + /cmdk@0.2.0(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@radix-ui/react-dialog': 1.0.0(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + command-score: 0.1.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -24549,7 +26014,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.3.6 + debug: 4.4.0 transitivePeerDependencies: - supports-color dev: true @@ -24877,14 +26342,14 @@ packages: dependencies: '@radix-ui/react-dialog': 1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-popover': 1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.37)(react@18.2.0) class-variance-authority: 0.7.1 clsx: 2.1.1 cmdk: 0.2.0(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-easy-sort: 1.6.0(react-dom@18.2.0)(react@18.2.0) - tailwind-merge: 2.5.5 + tailwind-merge: 2.6.0 tsup: 6.7.0(postcss@8.4.41)(ts-node@10.9.1)(typescript@5.1.6) transitivePeerDependencies: - '@swc/core' @@ -24896,6 +26361,33 @@ packages: - typescript dev: false + /emblor@1.4.6(@types/react-dom@18.2.15)(@types/react@18.2.37)(postcss@8.4.41)(react-dom@18.2.0)(react@18.2.0)(ts-node@10.9.1)(typescript@5.5.4): + resolution: {integrity: sha512-ay0Y74xdsnuxI662153ps65wPTBC7l457NPQm+eTUrrtwMzs+Pg4taULQrGvjHoBeMOZ4W2q6/KvNg8mur3PTA==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@radix-ui/react-dialog': 1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popover': 1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.37)(react@18.2.0) + class-variance-authority: 0.7.1 + clsx: 2.1.1 + cmdk: 0.2.0(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-easy-sort: 1.6.0(react-dom@18.2.0)(react@18.2.0) + tailwind-merge: 2.6.0 + tsup: 6.7.0(postcss@8.4.41)(ts-node@10.9.1)(typescript@5.5.4) + transitivePeerDependencies: + - '@swc/core' + - '@types/react' + - '@types/react-dom' + - postcss + - supports-color + - ts-node + - typescript + dev: false + /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -25584,7 +27076,16 @@ packages: eslint: 8.54.0 dev: true - /eslint-config-standard-with-typescript@37.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.54.0)(typescript@5.5.4): + /eslint-config-prettier@9.0.0(eslint@8.56.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.56.0 + dev: false + + /eslint-config-standard-with-typescript@37.0.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.54.0)(typescript@5.5.4): resolution: {integrity: sha512-V8I/Q1eFf9tiOuFHkbksUdWO3p1crFmewecfBtRxXdnvb71BCJx+1xAknlIRZMwZioMX3/bPtMVCZsf1+AjjOw==} peerDependencies: '@typescript-eslint/eslint-plugin': ^5.52.0 @@ -25594,11 +27095,10 @@ packages: eslint-plugin-promise: ^6.0.0 typescript: '*' dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.54.0)(typescript@5.5.4) '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.5.4) eslint: 8.54.0 eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.54.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint@8.54.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.54.0) eslint-plugin-n: 16.6.2(eslint@8.54.0) eslint-plugin-promise: 6.1.1(eslint@8.54.0) typescript: 5.5.4 @@ -25616,7 +27116,7 @@ packages: eslint-plugin-promise: ^6.0.0 dependencies: eslint: 8.54.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint@8.54.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.54.0) eslint-plugin-n: 16.6.2(eslint@8.54.0) eslint-plugin-promise: 6.1.1(eslint@8.54.0) dev: true @@ -25742,6 +27242,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.14.0(eslint@8.54.0)(typescript@5.5.4) + debug: 3.2.7 + eslint: 8.54.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-astro@0.28.0(eslint@8.54.0): resolution: {integrity: sha512-fZ3B93nXLSXMmEYSAnHkDRBKDbUFuIkWj5CoKE4fxjPnE/EZEHu6zxtX2UJZeclJKu33Uf2mWdeCJKFufyracg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -25935,7 +27464,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0)(eslint@8.54.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.54.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -25945,7 +27474,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.14.0(eslint@8.54.0)(typescript@5.5.4) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 @@ -25954,7 +27483,7 @@ packages: doctrine: 2.1.0 eslint: 8.54.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -25998,6 +27527,22 @@ packages: eslint: 8.53.0 dev: false + /eslint-plugin-prefer-arrow@1.2.3(eslint@8.54.0): + resolution: {integrity: sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ==} + peerDependencies: + eslint: '>=2.0.0' + dependencies: + eslint: 8.54.0 + dev: true + + /eslint-plugin-prefer-arrow@1.2.3(eslint@8.56.0): + resolution: {integrity: sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ==} + peerDependencies: + eslint: '>=2.0.0' + dependencies: + eslint: 8.56.0 + dev: false + /eslint-plugin-promise@6.1.1(eslint@8.54.0): resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -26092,6 +27637,31 @@ packages: string.prototype.matchall: 4.0.10 dev: true + /eslint-plugin-react@7.33.2(eslint@8.54.0): + resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.7 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.2 + doctrine: 2.1.0 + es-iterator-helpers: 1.0.15 + eslint: 8.54.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.7 + object.fromentries: 2.0.7 + object.hasown: 1.1.3 + object.values: 1.1.7 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.10 + dev: true + /eslint-plugin-react@7.33.2(eslint@8.56.0): resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} engines: {node: '>=4'} @@ -26262,6 +27832,21 @@ packages: eslint-rule-composer: 0.3.0 dev: false + /eslint-plugin-unused-imports@3.0.0(@typescript-eslint/eslint-plugin@6.14.0)(eslint@8.56.0): + resolution: {integrity: sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^6.0.0 + eslint: ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.5.4) + eslint: 8.56.0 + eslint-rule-composer: 0.3.0 + dev: false + /eslint-rule-composer@0.3.0: resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} engines: {node: '>=4.0.0'} @@ -28329,7 +29914,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: agent-base: 5.1.1 - debug: 4.3.6 + debug: 4.4.0 transitivePeerDependencies: - supports-color dev: true @@ -28348,7 +29933,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.6 + debug: 4.4.0 transitivePeerDependencies: - supports-color dev: true @@ -31778,7 +33363,7 @@ packages: resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} dependencies: '@types/debug': 4.1.12 - debug: 4.3.6 + debug: 4.4.0 decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -31802,7 +33387,7 @@ packages: resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} dependencies: '@types/debug': 4.1.12 - debug: 4.3.6 + debug: 4.4.0 decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.1 @@ -34232,6 +35817,23 @@ packages: - encoding dev: false + /react-json-view@1.21.3(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==} + peerDependencies: + react: ^17.0.0 || ^16.3.0 || ^15.5.4 + react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4 + dependencies: + flux: 4.0.4(react@18.2.0) + react: 18.2.0 + react-base16-styling: 0.6.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + react-textarea-autosize: 8.5.3(@types/react@18.2.43)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - encoding + dev: false + /react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==} peerDependencies: @@ -34290,7 +35892,6 @@ packages: - encoding - react - ts-node - dev: true /react-phone-input-2@2.15.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-W03abwhXcwUoq+vUFvC6ch2+LJYMN8qSOiO889UH6S7SyMCQvox/LF3QWt+cZagZrRdi5z2ON3omnjoCUmlaYw==} @@ -34342,7 +35943,6 @@ packages: react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.2.43)(react@18.2.0) tslib: 2.6.2 - dev: true /react-remove-scroll@2.5.4(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==} @@ -34363,6 +35963,25 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.37)(react@18.2.0) dev: false + /react-remove-scroll@2.5.4(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.43 + react: 18.2.0 + react-remove-scroll-bar: 2.3.4(@types/react@18.2.43)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.43)(react@18.2.0) + tslib: 2.6.2 + use-callback-ref: 1.3.0(@types/react@18.2.43)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.43)(react@18.2.0) + dev: false + /react-remove-scroll@2.5.5(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} engines: {node: '>=10'} @@ -34398,7 +36017,6 @@ packages: tslib: 2.6.2 use-callback-ref: 1.3.0(@types/react@18.2.43)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.43)(react@18.2.0) - dev: true /react-resize-detector@7.1.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==} @@ -34507,7 +36125,6 @@ packages: invariant: 2.2.4 react: 18.2.0 tslib: 2.6.2 - dev: true /react-textarea-autosize@8.5.3(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==} @@ -34523,6 +36140,20 @@ packages: - '@types/react' dev: false + /react-textarea-autosize@8.5.3(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.23.8 + react: 18.2.0 + use-composed-ref: 1.3.0(react@18.2.0) + use-latest: 1.2.1(@types/react@18.2.43)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} peerDependencies: @@ -36649,8 +38280,8 @@ packages: resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==} dev: false - /tailwind-merge@2.5.5: - resolution: {integrity: sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==} + /tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} dev: false /tailwindcss-animate@1.0.5(tailwindcss@3.3.5): @@ -37183,6 +38814,24 @@ packages: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} dev: false + /ts-api-utils@1.0.3(typescript@4.9.3): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 4.9.3 + dev: true + + /ts-api-utils@1.0.3(typescript@5.1.6): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.1.6 + dev: true + /ts-api-utils@1.0.3(typescript@5.2.2): resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} engines: {node: '>=16.13.0'} @@ -37199,7 +38848,6 @@ packages: typescript: '>=4.2.0' dependencies: typescript: 5.5.4 - dev: false /ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} @@ -37576,7 +39224,7 @@ packages: bundle-require: 4.2.1(esbuild@0.17.19) cac: 6.7.14 chokidar: 3.5.3 - debug: 4.3.6 + debug: 4.4.0 esbuild: 0.17.19 execa: 5.1.1 globby: 11.1.0 @@ -37594,6 +39242,43 @@ packages: - ts-node dev: false + /tsup@6.7.0(postcss@8.4.41)(ts-node@10.9.1)(typescript@5.5.4): + resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} + engines: {node: '>=14.18'} + hasBin: true + peerDependencies: + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.1.0' + peerDependenciesMeta: + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.2.1(esbuild@0.17.19) + cac: 6.7.14 + chokidar: 3.5.3 + debug: 4.4.0 + esbuild: 0.17.19 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss: 8.4.41 + postcss-load-config: 3.1.4(postcss@8.4.41)(ts-node@10.9.1) + resolve-from: 5.0.0 + rollup: 3.29.4 + source-map: 0.8.0-beta.0 + sucrase: 3.34.0 + tree-kill: 1.2.2 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + - ts-node + dev: false + /tsutils@3.21.0(typescript@4.9.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -38209,7 +39894,6 @@ packages: '@types/react': 18.2.43 react: 18.2.0 tslib: 2.6.2 - dev: true /use-composed-ref@1.3.0(react@18.2.0): resolution: {integrity: sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==} @@ -38241,6 +39925,19 @@ packages: react: 18.2.0 dev: false + /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.43 + react: 18.2.0 + dev: false + /use-latest@1.2.1(@types/react@18.2.37)(react@18.2.0): resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==} peerDependencies: @@ -38255,6 +39952,20 @@ packages: use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.37)(react@18.2.0) dev: false + /use-latest@1.2.1(@types/react@18.2.43)(react@18.2.0): + resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.43 + react: 18.2.0 + use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.43)(react@18.2.0) + dev: false + /use-query-params@2.2.1(react-dom@18.2.0)(react-router-dom@6.19.0)(react@18.2.0): resolution: {integrity: sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==} peerDependencies: @@ -38314,7 +40025,6 @@ packages: detect-node-es: 1.1.0 react: 18.2.0 tslib: 2.6.2 - dev: true /use-sync-external-store@1.2.0(react@18.2.0): resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} From 49869ff3ce6fef37e625e2353d3427b34e6ab304 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 27 Dec 2024 15:29:33 +0200 Subject: [PATCH 32/54] feat: added v2 adapters for custom fields & updated exports from ui & added tests (#2913) --- .../fields/CountryPicker/CountryPicker.tsx | 29 ++++++ .../CountryPicker/CountryPicker.unit.test.tsx | 78 +++++++++++++++ .../form/fields/CountryPicker/index.ts | 1 + .../IndustriesPicker/IndustriesPicker.tsx | 30 ++++++ .../IndustriesPicker.unit.test.tsx | 62 ++++++++++++ .../form/fields/IndustriesPicker/index.ts | 1 + .../form/fields/LocalePicker/LocalePicker.tsx | 33 +++++++ .../LocalePicker/LocalePicker.unit.test.tsx | 65 +++++++++++++ .../form/fields/LocalePicker/index.ts | 1 + .../form/fields/MCCPicker/MCCPicker.tsx | 24 +++++ .../fields/MCCPicker/MCCPicker.unit.test.tsx | 63 ++++++++++++ .../components/form/fields/MCCPicker/index.ts | 1 + .../NationalityPicker/NationalityPicker.tsx | 33 +++++++ .../NationalityPicker.unit.test.tsx | 75 +++++++++++++++ .../form/fields/NationalityPicker/index.ts | 1 + .../form/fields/StatePicker/StatePicker.tsx | 43 +++++++++ .../StatePicker/StatePicker.unit.test.tsx | 95 +++++++++++++++++++ .../form/fields/StatePicker/index.ts | 1 + .../fields/TagsField/TagsField.tsx | 4 +- .../Form/DynamicForm/fields/index.ts | 5 + .../organisms/Form/DynamicForm/index.ts | 3 + 21 files changed, 646 insertions(+), 2 deletions(-) create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/index.ts diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.tsx new file mode 100644 index 0000000000..4efd23f511 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.tsx @@ -0,0 +1,29 @@ +import { getCountries } from '@/helpers/countries-data'; +import { useLanguageParam } from '@/hooks/useLanguageParam/useLanguageParam'; +import { IFormElement, ISelectFieldParams, SelectField, TDynamicFormField } from '@ballerine/ui'; +import { useMemo } from 'react'; + +export const COUNTRY_PICKER_FIELD_TYPE = 'countrypickerfield'; + +export const CountryPickerField: TDynamicFormField = ({ element }) => { + const { language } = useLanguageParam(); + + const elementDefinitionWithCountryList: IFormElement< + typeof COUNTRY_PICKER_FIELD_TYPE, + ISelectFieldParams + > = useMemo(() => { + return { + ...element, + element: COUNTRY_PICKER_FIELD_TYPE, + params: { + ...element.params, + options: getCountries(language).map(country => ({ + value: country.const, + label: country.title, + })), + }, + }; + }, [element, language]); + + return ; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.unit.test.tsx new file mode 100644 index 0000000000..8dfceaa801 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.unit.test.tsx @@ -0,0 +1,78 @@ +import { getCountries } from '@/helpers/countries-data'; +import { useLanguageParam } from '@/hooks/useLanguageParam/useLanguageParam'; +import { IFormElement, ISelectFieldParams } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { COUNTRY_PICKER_FIELD_TYPE, CountryPickerField } from './CountryPicker'; + +// Mock dependencies +vi.mock('@/helpers/countries-data'); +vi.mock('@/hooks/useLanguageParam/useLanguageParam'); +vi.mock('@ballerine/ui', () => ({ + SelectField: ({ element }: { element: any }) => ( +
{JSON.stringify(element)}
+ ), +})); + +describe('CountryPickerField', () => { + const mockElement = { + params: {}, + } as IFormElement; + + const mockCountries = [ + { const: 'US', title: 'United States' }, + { const: 'GB', title: 'United Kingdom' }, + ]; + + beforeEach(() => { + vi.mocked(getCountries).mockReturnValue(mockCountries); + vi.mocked(useLanguageParam).mockReturnValue({ language: 'en', setLanguage: vi.fn() }); + }); + + it('renders SelectField with transformed country options', () => { + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect(elementProp).toEqual({ + element: COUNTRY_PICKER_FIELD_TYPE, + params: { + options: [ + { value: 'US', label: 'United States' }, + { value: 'GB', label: 'United Kingdom' }, + ], + }, + }); + }); + + it('uses language from useLanguageParam hook to get countries', () => { + vi.mocked(useLanguageParam).mockReturnValue({ language: 'cn', setLanguage: vi.fn() }); + + render(); + + expect(getCountries).toHaveBeenCalledWith('cn'); + }); + + it('preserves existing element params while adding options', () => { + const elementWithParams = { + ...mockElement, + params: { + placeholder: 'Select a country', + }, + } as IFormElement; + + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect((elementProp as any).params).toEqual({ + placeholder: 'Select a country', + options: [ + { value: 'US', label: 'United States' }, + { value: 'GB', label: 'United Kingdom' }, + ], + }); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/index.ts new file mode 100644 index 0000000000..5617ef2e23 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/index.ts @@ -0,0 +1 @@ +export * from './CountryPicker'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.tsx new file mode 100644 index 0000000000..aa4b8a7702 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.tsx @@ -0,0 +1,30 @@ +import { IFormElement, ISelectFieldParams, SelectField, TDynamicFormField } from '@ballerine/ui'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const INDUSTRIES_PICKER_FIELD_TYPE = 'industriespickerfield'; + +export const IndustriesPickerField: TDynamicFormField = ({ element }) => { + const { t } = useTranslation(); + + const translatedIndustries = t('industries', { returnObjects: true }) as string[]; + + const elementDefinitionWithIndustriesList: IFormElement< + typeof INDUSTRIES_PICKER_FIELD_TYPE, + ISelectFieldParams + > = useMemo(() => { + return { + ...element, + element: INDUSTRIES_PICKER_FIELD_TYPE, + params: { + ...element.params, + options: translatedIndustries.map(industry => ({ + value: industry, + label: industry, + })), + }, + }; + }, [element, translatedIndustries]); + + return ; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.unit.test.tsx new file mode 100644 index 0000000000..2c72fee134 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.unit.test.tsx @@ -0,0 +1,62 @@ +import { IFormElement, ISelectFieldParams } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { INDUSTRIES_PICKER_FIELD_TYPE, IndustriesPickerField } from './IndustriesPicker'; + +// Mock dependencies +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: vi.fn().mockReturnValue(['Industry1', 'Industry2']), + }), +})); + +vi.mock('@ballerine/ui', () => ({ + SelectField: ({ element }: { element: any }) => ( +
{JSON.stringify(element)}
+ ), +})); + +describe('IndustriesPickerField', () => { + const mockElement = { + params: {}, + } as IFormElement; + + it('renders SelectField with transformed industry options', () => { + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect(elementProp).toEqual({ + element: INDUSTRIES_PICKER_FIELD_TYPE, + params: { + options: [ + { value: 'Industry1', label: 'Industry1' }, + { value: 'Industry2', label: 'Industry2' }, + ], + }, + }); + }); + + it('preserves existing element params while adding options', () => { + const elementWithParams = { + ...mockElement, + params: { + placeholder: 'Select an industry', + }, + } as IFormElement; + + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect((elementProp as any).params).toEqual({ + placeholder: 'Select an industry', + options: [ + { value: 'Industry1', label: 'Industry1' }, + { value: 'Industry2', label: 'Industry2' }, + ], + }); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/index.ts new file mode 100644 index 0000000000..8f8fb48ed0 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/index.ts @@ -0,0 +1 @@ +export * from './IndustriesPicker'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.tsx new file mode 100644 index 0000000000..204703c060 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.tsx @@ -0,0 +1,33 @@ +import { IFormElement, ISelectFieldParams, SelectField, TDynamicFormField } from '@ballerine/ui'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const LOCALE_PICKER_FIELD_TYPE = 'localePickerField'; + +export const LocalePickerField: TDynamicFormField = ({ element }) => { + const { t } = useTranslation(); + + const elementDefinitionWithLocaleList: IFormElement< + typeof LOCALE_PICKER_FIELD_TYPE, + ISelectFieldParams + > = useMemo(() => { + return { + ...element, + element: LOCALE_PICKER_FIELD_TYPE, + params: { + ...element.params, + options: ( + t('languages', { returnObjects: true }) as Array<{ + const: string; + title: string; + }> + ).map(locale => ({ + value: locale.const, + label: locale.title, + })), + }, + }; + }, [element, t]); + + return ; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.unit.test.tsx new file mode 100644 index 0000000000..a5b4e7cb20 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.unit.test.tsx @@ -0,0 +1,65 @@ +import { IFormElement, ISelectFieldParams } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { LOCALE_PICKER_FIELD_TYPE, LocalePickerField } from './LocalePicker'; + +// Mock dependencies +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: vi.fn().mockReturnValue([ + { const: 'en', title: 'English' }, + { const: 'es', title: 'Spanish' }, + ]), + }), +})); + +vi.mock('@ballerine/ui', () => ({ + SelectField: ({ element }: { element: any }) => ( +
{JSON.stringify(element)}
+ ), +})); + +describe('LocalePickerField', () => { + const mockElement = { + params: {}, + } as IFormElement; + + it('renders SelectField with transformed locale options', () => { + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect(elementProp).toEqual({ + element: LOCALE_PICKER_FIELD_TYPE, + params: { + options: [ + { value: 'en', label: 'English' }, + { value: 'es', label: 'Spanish' }, + ], + }, + }); + }); + + it('preserves existing element params while adding options', () => { + const elementWithParams = { + ...mockElement, + params: { + placeholder: 'Select a language', + }, + } as IFormElement; + + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect((elementProp as any).params).toEqual({ + placeholder: 'Select a language', + options: [ + { value: 'en', label: 'English' }, + { value: 'es', label: 'Spanish' }, + ], + }); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/index.ts new file mode 100644 index 0000000000..f8123afa3f --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/index.ts @@ -0,0 +1 @@ +export * from './LocalePicker'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.tsx new file mode 100644 index 0000000000..b235cd2869 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.tsx @@ -0,0 +1,24 @@ +import { MCC } from '@/components/organisms/UIRenderer/elements/JSONForm/components/MCCPicker/options'; +import { IFormElement, ISelectFieldParams, SelectField, TDynamicFormField } from '@ballerine/ui'; +import { useMemo } from 'react'; + +export const MCC_PICKER_FIELD_TYPE = 'mccpickerfield'; + +export const MCCPickerField: TDynamicFormField = ({ element }) => { + const elementWithMccOptions: IFormElement = + useMemo(() => { + return { + ...element, + element: MCC_PICKER_FIELD_TYPE, + params: { + ...element.params, + options: MCC.map(item => ({ + value: item.const, + label: `${item.const} - ${item.title}`, + })), + }, + }; + }, [element]); + + return ; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.unit.test.tsx new file mode 100644 index 0000000000..3dfea66a4f --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.unit.test.tsx @@ -0,0 +1,63 @@ +import { IFormElement, ISelectFieldParams } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { MCC_PICKER_FIELD_TYPE, MCCPickerField } from './MCCPicker'; + +// Mock dependencies +vi.mock('@/components/organisms/UIRenderer/elements/JSONForm/components/MCCPicker/options', () => ({ + MCC: [ + { const: '1234', title: 'Test MCC 1' }, + { const: '5678', title: 'Test MCC 2' }, + ], +})); + +vi.mock('@ballerine/ui', () => ({ + SelectField: ({ element }: { element: any }) => ( +
{JSON.stringify(element)}
+ ), +})); + +describe('MCCPickerField', () => { + const mockElement = { + params: {}, + } as IFormElement; + + it('renders SelectField with transformed MCC options', () => { + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect(elementProp).toEqual({ + element: MCC_PICKER_FIELD_TYPE, + params: { + options: [ + { value: '1234', label: '1234 - Test MCC 1' }, + { value: '5678', label: '5678 - Test MCC 2' }, + ], + }, + }); + }); + + it('preserves existing element params while adding options', () => { + const elementWithParams = { + ...mockElement, + params: { + placeholder: 'Select an MCC', + }, + } as IFormElement; + + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect((elementProp as any).params).toEqual({ + placeholder: 'Select an MCC', + options: [ + { value: '1234', label: '1234 - Test MCC 1' }, + { value: '5678', label: '5678 - Test MCC 2' }, + ], + }); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/index.ts new file mode 100644 index 0000000000..558c697e74 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/index.ts @@ -0,0 +1 @@ +export * from './MCCPicker'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.tsx new file mode 100644 index 0000000000..9a4bf9c275 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.tsx @@ -0,0 +1,33 @@ +import { getNationalities } from '@/helpers/countries-data'; +import { useLanguageParam } from '@/hooks/useLanguageParam/useLanguageParam'; +import { IFormElement, ISelectFieldParams, SelectField, TDynamicFormField } from '@ballerine/ui'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const NATIONALITY_PICKER_FIELD_TYPE = 'nationalitypickerfield'; + +export const NationalityPickerField: TDynamicFormField = ({ element }) => { + const { language } = useLanguageParam(); + const { t } = useTranslation(); + + const elementWithNationalities: IFormElement< + typeof NATIONALITY_PICKER_FIELD_TYPE, + ISelectFieldParams + > = useMemo(() => { + const nationalities = getNationalities(language, t); + + return { + ...element, + element: NATIONALITY_PICKER_FIELD_TYPE, + params: { + ...element.params, + options: nationalities.map(nationality => ({ + value: nationality.const, + label: nationality.title, + })), + }, + }; + }, [element, language, t]); + + return ; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.unit.test.tsx new file mode 100644 index 0000000000..6545aff1f4 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.unit.test.tsx @@ -0,0 +1,75 @@ +import { IFormElement, ISelectFieldParams } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { NATIONALITY_PICKER_FIELD_TYPE, NationalityPickerField } from './NationalityPicker'; + +// Mock dependencies +vi.mock('@/hooks/useLanguageParam/useLanguageParam', () => ({ + useLanguageParam: () => ({ + language: 'en', + }), +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: vi.fn().mockImplementation(key => key), + }), +})); + +vi.mock('@/helpers/countries-data', () => ({ + getNationalities: vi.fn().mockReturnValue([ + { const: 'US', title: 'American' }, + { const: 'GB', title: 'British' }, + ]), +})); + +vi.mock('@ballerine/ui', () => ({ + SelectField: ({ element }: { element: any }) => ( +
{JSON.stringify(element)}
+ ), +})); + +describe('NationalityPickerField', () => { + const mockElement = { + params: {}, + } as IFormElement; + + it('renders SelectField with transformed nationality options', () => { + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect(elementProp).toEqual({ + element: NATIONALITY_PICKER_FIELD_TYPE, + params: { + options: [ + { value: 'US', label: 'American' }, + { value: 'GB', label: 'British' }, + ], + }, + }); + }); + + it('preserves existing element params while adding options', () => { + const elementWithParams = { + ...mockElement, + params: { + placeholder: 'Select a nationality', + }, + } as IFormElement; + + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect((elementProp as any).params).toEqual({ + placeholder: 'Select a nationality', + options: [ + { value: 'US', label: 'American' }, + { value: 'GB', label: 'British' }, + ], + }); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/index.ts new file mode 100644 index 0000000000..057c6cf25f --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/index.ts @@ -0,0 +1 @@ +export * from './NationalityPicker'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.tsx new file mode 100644 index 0000000000..24598c1176 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.tsx @@ -0,0 +1,43 @@ +import { getCountryStates } from '@/helpers/countries-data'; +import { + IFormElement, + ISelectFieldParams, + SelectField, + TDynamicFormField, + useDynamicForm, +} from '@ballerine/ui'; +import get from 'lodash/get'; +import { useMemo } from 'react'; + +export const STATE_PICKER_FIELD_TYPE = 'statepickerfield'; + +export interface IStatePickerParams extends ISelectFieldParams { + countryCodePath?: string; +} + +export const StatePickerField: TDynamicFormField = ({ element }) => { + const { countryCodePath } = element.params || {}; + const { values } = useDynamicForm(); + + const options = useMemo(() => { + const countryCode = get(values, countryCodePath || '') as string | null; + + return countryCode + ? getCountryStates(countryCode).map(state => ({ title: state.name, const: state.isoCode })) + : []; + }, [values, countryCodePath]); + + const elementWithStateOptions: IFormElement = + useMemo(() => { + return { + ...element, + element: STATE_PICKER_FIELD_TYPE, + params: { + ...element.params, + options: options.map(option => ({ value: option.const, label: option.title })), + }, + }; + }, [element, options]); + + return ; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.unit.test.tsx new file mode 100644 index 0000000000..0e0c2f5cf8 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.unit.test.tsx @@ -0,0 +1,95 @@ +import { IFormElement, ISelectFieldParams, useDynamicForm } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { STATE_PICKER_FIELD_TYPE, StatePickerField } from './StatePicker'; + +// Mock dependencies +vi.mock('@ballerine/ui', () => ({ + ...vi.importActual('@ballerine/ui'), + SelectField: ({ element }: { element: any }) => ( +
{JSON.stringify(element)}
+ ), + useDynamicForm: vi.fn().mockReturnValue({ + values: { + country: 'US', + }, + }), +})); + +vi.mock('@/helpers/countries-data', () => ({ + getCountryStates: vi.fn().mockReturnValue([ + { name: 'California', isoCode: 'CA' }, + { name: 'New York', isoCode: 'NY' }, + ]), +})); + +describe('StatePickerField', () => { + const mockElement = { + params: { + countryCodePath: 'country', + }, + } as unknown as IFormElement; + + it('renders SelectField with transformed state options when country is selected', () => { + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect(elementProp).toEqual({ + element: STATE_PICKER_FIELD_TYPE, + params: { + countryCodePath: 'country', + options: [ + { value: 'CA', label: 'California' }, + { value: 'NY', label: 'New York' }, + ], + }, + }); + }); + + it('preserves existing element params while adding options', () => { + const elementWithParams = { + params: { + countryCodePath: 'country', + placeholder: 'Select a state', + }, + } as unknown as IFormElement; + + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect(elementProp).toEqual({ + element: STATE_PICKER_FIELD_TYPE, + params: { + countryCodePath: 'country', + placeholder: 'Select a state', + options: [ + { value: 'CA', label: 'California' }, + { value: 'NY', label: 'New York' }, + ], + }, + }); + }); + + it('returns empty options when no country is selected', () => { + vi.mocked(useDynamicForm).mockReturnValueOnce({ + values: {}, + } as ReturnType); + + render(); + + const selectField = screen.getByTestId('select-field'); + const elementProp = JSON.parse(selectField.textContent || ''); + + expect(elementProp).toEqual({ + element: STATE_PICKER_FIELD_TYPE, + params: { + countryCodePath: 'country', + options: [], + }, + }); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/index.ts new file mode 100644 index 0000000000..46d13b26dc --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/index.ts @@ -0,0 +1 @@ +export * from './StatePicker'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx index 69ceb20e56..7501006fe4 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx @@ -6,9 +6,9 @@ import { FieldLayout } from '../../layouts/FieldLayout'; import { ICommonFieldParams, TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; -export type ITextFieldParams = ICommonFieldParams; +export type ITagsFieldParams = ICommonFieldParams; -export const TagsField: TDynamicFormField = ({ element }) => { +export const TagsField: TDynamicFormField = ({ element }) => { const { stack } = useStack(); const { value, onChange, onBlur, onFocus, disabled } = useField( element, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts index 6ac7454e90..f18d1beb98 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/index.ts @@ -3,5 +3,10 @@ export * from './CheckboxField'; export * from './CheckboxList'; export * from './DateField'; export * from './FieldList'; +export * from './FileField'; export * from './MultiselectField'; +export * from './PhoneField'; +export * from './RadioField'; +export * from './SelectField'; +export * from './TagsField'; export * from './TextField'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/index.ts index 5b26b998f6..eb74bf18ce 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/index.ts @@ -1,2 +1,5 @@ +export * from './context/hooks/useDynamicForm'; export * from './DynamicForm'; +export * from './fields'; export * from './hooks/external'; +export * from './types'; From 82d8388bb372649e38395f40cdd8d4ce34f699e3 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 27 Dec 2024 16:11:36 +0200 Subject: [PATCH 33/54] feat: added field descriptions & updated tests (#2914) --- .../InputsShowcase/InputsShowcase.tsx | 13 ++ .../AutocompleteField/AutocompleteField.tsx | 2 + .../fields/CheckboxField/CheckboxField.tsx | 39 ++-- .../CheckboxField/CheckboxField.unit.test.tsx | 199 ++++++------------ .../fields/CheckboxList/CheckboxList.tsx | 2 + .../CheckboxList/CheckboxList.unit.test.tsx | 36 +++- .../fields/DateField/DateField.tsx | 2 + .../fields/DateField/DateField.unit.test.tsx | 12 ++ .../fields/FieldList/FieldList.tsx | 2 + .../fields/FieldList/FieldList.unit.test.tsx | 12 ++ .../fields/FileField/FileField.tsx | 2 + .../fields/FileField/FileField.unit.test.tsx | 10 + .../MultiselectField/MultiselectField.tsx | 2 + .../MultiselectField.unit.test.tsx | 13 ++ .../fields/PhoneField/PhoneField.tsx | 2 + .../PhoneField/PhoneField.unit.test.tsx | 16 ++ .../fields/RadioField/RadioField.tsx | 2 + .../RadioField/RadioField.unit.test.tsx | 32 +++ .../fields/SelectField/SelectField.tsx | 2 + .../SelectField/SelectField.unit.test.tsx | 16 ++ .../fields/TagsField/TagsField.tsx | 2 + .../fields/TagsField/TagsField.unit.test.tsx | 16 ++ .../fields/TextField/TextField.tsx | 2 + .../fields/TextField/TextField.unit.test.tsx | 16 ++ .../FieldDescription/FieldDescription.tsx | 14 ++ .../FieldDescription.unit.test.tsx | 43 ++++ .../layouts/FieldDescription/index.ts | 1 + .../layouts/FieldLayout/FieldLayout.tsx | 9 +- .../FieldLayout/FieldLayout.unit.test.tsx | 186 +++++----------- .../organisms/Form/DynamicForm/types/index.ts | 1 + .../useValidate/useValidate.unit.test.ts | 2 +- 31 files changed, 406 insertions(+), 302 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/index.ts 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 dd43c34b59..88c034e861 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 @@ -12,6 +12,7 @@ const schema: Array> = [ params: { label: 'Text Field', placeholder: 'Enter text', + description: 'This is a text field for entering any text value', }, validate: [], }, @@ -22,6 +23,7 @@ const schema: Array> = [ params: { label: 'Autocomplete Field', placeholder: 'Select an option', + description: 'This is an autocomplete field that provides suggestions as you type', options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, @@ -35,6 +37,7 @@ const schema: Array> = [ valueDestination: 'checkboxlist', params: { label: 'Checkbox List Field', + description: 'Select multiple options from this list of checkboxes', options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, @@ -48,6 +51,7 @@ const schema: Array> = [ valueDestination: 'date', params: { label: 'Date Field', + description: 'Select a date from the calendar', }, }, { @@ -56,6 +60,7 @@ const schema: Array> = [ valueDestination: 'multiselect', params: { label: 'Multiselect Field', + description: 'Select multiple options from the dropdown list', options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, @@ -69,6 +74,7 @@ const schema: Array> = [ valueDestination: 'select', params: { label: 'Select Field', + description: 'Choose a single option from the dropdown list', options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, @@ -82,6 +88,7 @@ const schema: Array> = [ valueDestination: 'checkbox', params: { label: 'Checkbox Field', + description: 'Toggle this checkbox for a yes/no selection', }, }, { @@ -90,6 +97,7 @@ const schema: Array> = [ valueDestination: 'phone', params: { label: 'Phone Field', + description: 'Enter a phone number with country code selection', defaultCountry: 'il', }, }, @@ -99,6 +107,7 @@ const schema: Array> = [ valueDestination: 'radio', params: { label: 'Radio Field', + description: 'Select one option from these radio buttons', options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, @@ -112,6 +121,7 @@ const schema: Array> = [ valueDestination: 'tags', params: { label: 'Tags Field', + description: 'Add multiple tags by typing and pressing enter', }, }, { @@ -121,6 +131,7 @@ const schema: Array> = [ params: { label: 'File Field', placeholder: 'Select File', + description: 'Upload a file from your device', }, }, { @@ -129,6 +140,7 @@ const schema: Array> = [ valueDestination: 'fieldlist', params: { label: 'Field List', + description: 'A list of repeatable form fields that can be added or removed', }, children: [ { @@ -138,6 +150,7 @@ const schema: Array> = [ params: { label: 'Text Field', placeholder: 'Enter text', + description: 'Enter text for this list item', }, validate: [ { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx index 4ff28ea7ac..25cc14708a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx @@ -9,6 +9,7 @@ import { useStack } from '../FieldList/providers/StackProvider'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; export interface IAutocompleteFieldOption { label: string; @@ -46,6 +47,7 @@ export const AutocompleteField: TDynamicFormField = ({ onBlur={onBlur} onFocus={onFocus} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx index 468ddc2488..fef6a3c28c 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx @@ -1,34 +1,45 @@ -import { Checkbox } from '@/components/atoms'; +import { Checkbox, Label } from '@/components/atoms'; +import { useDynamicForm } from '../../context'; import { useElement, useField } from '../../hooks/external'; +import { useRequired } from '../../hooks/external/useRequired'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; -import { FieldLayout } from '../../layouts/FieldLayout'; -import { TDynamicFormField } from '../../types'; +import { ICommonFieldParams, TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; -export const CheckboxField: TDynamicFormField = ({ element }) => { +export const CheckboxField: TDynamicFormField = ({ element }) => { useMountEvent(element); useUnmountEvent(element); + const { label } = element.params || {}; const { stack } = useStack(); const { id } = useElement(element, stack); const { value, onChange, onFocus, onBlur, disabled } = useField( element, stack, ); + const { values } = useDynamicForm(); + const isRequired = useRequired(element, values); return ( - - +
+
+ + +
+ - +
); }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx index 3f2afafc92..1681e64dc6 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx @@ -1,189 +1,120 @@ -import { cleanup, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { IDynamicFormContext, useDynamicForm } from '../../context'; import { useElement, useField } from '../../hooks/external'; +import { useRequired } from '../../hooks/external/useRequired'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; -import { FieldErrors } from '../../layouts/FieldErrors'; -import { FieldLayout } from '../../layouts/FieldLayout'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { CheckboxField } from './CheckboxField'; -// Mock dependencies -vi.mock('@/components/atoms', () => ({ - Checkbox: vi.fn((props: any) => ( - props.onCheckedChange(e.target.checked)} - disabled={props.disabled} - onFocus={props.onFocus} - onBlur={props.onBlur} - data-testid="test-checkbox" - id={props.id} - /> - )), -})); - -vi.mock('../FieldList/providers/StackProvider', () => ({ - useStack: vi.fn(), -})); - -vi.mock('../../hooks/external/useField', () => ({ - useField: vi.fn(), -})); - -vi.mock('../../hooks/external/useElement', () => ({ - useElement: vi.fn(), -})); - -vi.mock('../../layouts/FieldLayout', () => ({ - FieldLayout: vi.fn(({ children }) =>
{children}
), -})); - -vi.mock('../../layouts/FieldErrors', () => ({ - FieldErrors: vi.fn(() =>
), -})); - -vi.mock('../../hooks/internal/useMountEvent', () => ({ - useMountEvent: vi.fn(), -})); - -vi.mock('../../hooks/internal/useUnmountEvent', () => ({ - useUnmountEvent: vi.fn(), -})); +vi.mock('../../context'); +vi.mock('../FieldList/providers/StackProvider'); +vi.mock('../../hooks/external'); +vi.mock('../../hooks/external/useRequired'); +vi.mock('../../hooks/internal/useMountEvent'); +vi.mock('../../hooks/internal/useUnmountEvent'); describe('CheckboxField', () => { - const mockStack = [0]; const mockElement = { - id: 'test-checkbox', - type: '', - } as unknown as IFormElement; - - const mockFieldProps = { - value: false, - onChange: vi.fn(), - onFocus: vi.fn(), - onBlur: vi.fn(), - disabled: false, - } as unknown as ReturnType; + id: 'test', + type: 'checkbox', + params: { + label: 'Test Label', + }, + } as unknown as IFormElement; + + const mockOnChange = vi.fn(); + const mockOnFocus = vi.fn(); + const mockOnBlur = vi.fn(); beforeEach(() => { - cleanup(); - vi.clearAllMocks(); - vi.mocked(useStack).mockReturnValue({ stack: mockStack }); - vi.mocked(useField).mockReturnValue(mockFieldProps); - vi.mocked(useElement).mockReturnValue({ id: 'test-checkbox-id' } as unknown as ReturnType< - typeof useElement - >); + vi.mocked(useDynamicForm).mockReturnValue({ + values: {}, + } as unknown as IDynamicFormContext); + vi.mocked(useStack).mockReturnValue({ stack: [] }); + vi.mocked(useElement).mockReturnValue({ id: 'test-id', originId: 'test-id', hidden: false }); + vi.mocked(useField).mockReturnValue({ + value: false, + onChange: mockOnChange, + onFocus: mockOnFocus, + onBlur: mockOnBlur, + disabled: false, + touched: false, + }); + vi.mocked(useRequired).mockReturnValue(false); + vi.mocked(useMountEvent).mockReturnValue(); + vi.mocked(useUnmountEvent).mockReturnValue(); }); - it('renders checkbox with correct initial state', () => { - render(); - const checkbox = screen.getByTestId('test-checkbox'); - expect(checkbox).toBeInTheDocument(); - expect(checkbox).not.toBeChecked(); - expect(checkbox).toHaveAttribute('id', 'test-checkbox-id'); + afterEach(() => { + vi.clearAllMocks(); }); - it('renders field layout and errors', () => { + it('renders checkbox with label', () => { render(); - expect(screen.getByTestId('field-layout')).toBeInTheDocument(); - expect(screen.getByTestId('field-errors')).toBeInTheDocument(); - expect(vi.mocked(FieldLayout)).toHaveBeenCalledWith( - expect.objectContaining({ - element: mockElement, - layout: 'horizontal', - }), - expect.any(Object), - ); + + expect(screen.getByRole('checkbox')).toBeInTheDocument(); + expect(screen.getByText('Test Label (optional)')).toBeInTheDocument(); }); - it('renders checked checkbox when value is true', () => { - vi.mocked(useField).mockReturnValue({ - ...mockFieldProps, - value: true, - }); + it('renders required label when isRequired is true', () => { + vi.mocked(useRequired).mockReturnValue(true); render(); - expect(screen.getByTestId('test-checkbox')).toBeChecked(); - }); - it('handles onChange events', async () => { - const mockOnChange = vi.fn(); - vi.mocked(useField).mockReturnValue({ - ...mockFieldProps, - onChange: mockOnChange, - }); + expect(screen.getByText('Test Label')).toBeInTheDocument(); + }); + it('handles checkbox state changes', async () => { render(); - const checkbox = screen.getByTestId('test-checkbox'); + const checkbox = screen.getByRole('checkbox'); await userEvent.click(checkbox); - expect(mockOnChange).toHaveBeenCalledWith(true); + expect(mockOnChange).toHaveBeenCalled(); }); it('handles focus events', async () => { - const user = userEvent.setup(); render(); - const checkbox = screen.getByTestId('test-checkbox'); - await user.click(checkbox); + screen.getByRole('checkbox'); + await userEvent.tab(); - expect(mockFieldProps.onFocus).toHaveBeenCalled(); + expect(mockOnFocus).toHaveBeenCalled(); }); it('handles blur events', async () => { - const user = userEvent.setup(); render(); - const checkbox = screen.getByTestId('test-checkbox'); - await user.click(checkbox); - await user.tab(); + const checkbox = screen.getByRole('checkbox'); + checkbox.focus(); + checkbox.blur(); - expect(mockFieldProps.onBlur).toHaveBeenCalled(); + expect(mockOnBlur).toHaveBeenCalled(); }); it('disables checkbox when disabled prop is true', () => { vi.mocked(useField).mockReturnValue({ - ...mockFieldProps, + value: false, + onChange: mockOnChange, + onFocus: mockOnFocus, + onBlur: mockOnBlur, disabled: true, + touched: false, }); render(); - expect(screen.getByTestId('test-checkbox')).toBeDisabled(); - }); - - it('handles undefined value as unchecked', () => { - vi.mocked(useField).mockReturnValue({ - ...mockFieldProps, - value: undefined, - }); - render(); - expect(screen.getByTestId('test-checkbox')).not.toBeChecked(); + expect(screen.getByRole('checkbox')).toBeDisabled(); }); - it('should call useMountEvent with element', () => { - const mockUseMountEvent = vi.mocked(useMountEvent); + it('calls mount and unmount events', () => { render(); - expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); - }); - it('should call useUnmountEvent with element', () => { - const mockUseUnmountEvent = vi.mocked(useUnmountEvent); - const { unmount } = render(); - unmount(); - expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); - }); - - it('should render FieldErrors with element prop', () => { - render(); - expect(FieldErrors).toHaveBeenCalledWith( - expect.objectContaining({ element: mockElement }), - expect.anything(), - ); + expect(useMountEvent).toHaveBeenCalledWith(mockElement); + expect(useUnmountEvent).toHaveBeenCalledWith(mockElement); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx index ef1ac01b72..c931b71d62 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx @@ -4,6 +4,7 @@ import { createTestId } from '@/components/organisms/Renderer'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -58,6 +59,7 @@ export const CheckboxListField: TDynamicFormField = ({ ))} + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx index a511257ffa..551dd72fbb 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx @@ -1,20 +1,15 @@ -import { ctw } from '@/common'; import { createTestId } from '@/components/organisms/Renderer'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { CheckboxListField, ICheckboxListFieldParams } from './CheckboxList'; -// Mock dependencies -vi.mock('@/common', () => ({ - ctw: vi.fn(), -})); - vi.mock('@/components/organisms/Renderer', () => ({ createTestId: vi.fn(), })); @@ -31,6 +26,10 @@ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + vi.mock('@/components/atoms', () => ({ Checkbox: vi.fn((props: any) => ( ({ onFocus={props.onFocus} onBlur={props.onBlur} className={props.className} + disabled={props.disabled} /> )), })); @@ -78,7 +78,6 @@ describe('CheckboxListField', () => { vi.clearAllMocks(); vi.mocked(createTestId).mockReturnValue('test-checkbox-list'); - vi.mocked(ctw).mockImplementation((...args: any[]) => args.filter(Boolean).join(' ')); vi.mocked(useStack).mockReturnValue({ stack: [] }); vi.mocked(useField).mockReturnValue({ @@ -192,6 +191,21 @@ describe('CheckboxListField', () => { expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); }); + it('disables all checkboxes when disabled is true', () => { + vi.mocked(useField).mockReturnValue({ + value: ['opt1'], + onChange: vi.fn(), + onFocus: vi.fn(), + onBlur: vi.fn(), + disabled: true, + } as unknown as ReturnType); + + render(); + + const container = screen.getByTestId('test-checkbox-list'); + expect(container).toHaveClass('pointer-events-none opacity-50'); + }); + it('should call useMountEvent with element', () => { const mockUseMountEvent = vi.mocked(useMountEvent); render(); @@ -204,6 +218,14 @@ describe('CheckboxListField', () => { expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); }); + it('should render FieldDescription with element prop', () => { + render(); + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); + it('should render FieldErrors with element prop', () => { render(); expect(FieldErrors).toHaveBeenCalledWith( diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx index bbb97e6c1f..150a92a0b5 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx @@ -9,6 +9,7 @@ import { useCallback } from 'react'; import { useField } from '../../hooks/external/useField'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -64,6 +65,7 @@ export const DateField: TDynamicFormField = ({ element }) => { onChange={handleChange} onFocus={onFocus} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx index 2a350aa578..5a62ac6358 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx @@ -6,6 +6,7 @@ import { useField } from '../../hooks/external/useField'; import { useEvents } from '../../hooks/internal/useEvents'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; import { DateField, IDateFieldParams } from './DateField'; @@ -53,6 +54,9 @@ vi.mock('../../hooks/internal/useUnmountEvent'); vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); describe('DateField', () => { beforeEach(() => { @@ -213,4 +217,12 @@ describe('DateField', () => { expect.anything(), ); }); + + it('should render FieldDescription with element prop', () => { + render(); + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx index d66ade5683..d469b417f2 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx @@ -5,6 +5,7 @@ import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { TDynamicFormField } from '../../types'; import { useFieldList } from './hooks/useFieldList'; @@ -59,6 +60,7 @@ export const FieldList: TDynamicFormField = props => {
+ ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx index 11e5af5ad3..682877e506 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx @@ -5,6 +5,7 @@ import { useElement } from '../../hooks/external'; import { useEvents } from '../../hooks/internal/useEvents'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; import { FieldList } from './FieldList'; @@ -20,6 +21,9 @@ vi.mock('../../hooks/internal/useUnmountEvent'); vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); vi.mock('@/components/atoms', () => ({ Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => ( @@ -168,4 +172,12 @@ describe('FieldList', () => { expect.anything(), ); }); + + it('should render FieldDescription with element prop', () => { + render(); + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx index 047d2d6306..4861f1ed7a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx @@ -7,6 +7,7 @@ import { useCallback, useMemo, useRef } from 'react'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { ICommonFieldParams, TDynamicFormField } from '../../types'; @@ -104,6 +105,7 @@ export const FileField: TDynamicFormField = ({ element }) => { className="hidden" /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx index 9667b4b8c3..44eea4b749 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx @@ -28,6 +28,9 @@ vi.mock('../../layouts/FieldLayout', () => ({ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(({ element }) =>
{element.id}
), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(({ element }) =>
{element.id}
), +})); describe('FileField', () => { const mockElement = { @@ -157,4 +160,11 @@ describe('FileField', () => { expect(useMountEvent).toHaveBeenCalledWith(mockElement); expect(useUnmountEvent).toHaveBeenCalledWith(mockElement); }); + + it('renders field description with element prop', () => { + render(); + const description = screen.getByTestId('field-description'); + expect(description).toBeInTheDocument(); + expect(description).toHaveTextContent(mockElement.id); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx index e89ea1971a..82abb57625 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -46,6 +47,7 @@ export const MultiselectField: TDynamicFormField = ({ e options={element.params?.options || []} renderSelected={renderSelected} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx index 9bb0c448d7..0e33020745 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx @@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { IFormElement } from '../../types'; @@ -52,6 +53,10 @@ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + vi.mock('../../layouts/FieldLayout', () => ({ FieldLayout: vi.fn(({ children }) =>
{children}
), })); @@ -229,4 +234,12 @@ describe('MultiselectField', () => { expect.anything(), ); }); + + it('should render FieldDescription with element prop', () => { + render(); + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx index b7a48f0215..cbd61a5883 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormElement } from '../../types'; @@ -38,6 +39,7 @@ export const PhoneField: TDynamicFormElement = ({ ele onBlur={onBlur} onFocus={onFocus} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx index 6e0eb28f9d..bdbcab4b46 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx @@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { IFormElement } from '../../types'; @@ -32,6 +33,10 @@ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + vi.mock('../../layouts/FieldLayout', () => ({ FieldLayout: vi.fn(({ children }) =>
{children}
), })); @@ -122,6 +127,17 @@ describe('PhoneField', () => { ); }); + it('should render FieldDescription with element prop', () => { + render(); + + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.anything(), + ); + }); + it('should pass stack to createTestId', () => { const mockStack = [0, 1]; vi.mocked(useStack).mockReturnValue({ stack: mockStack }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx index 63f179666b..ac4220db03 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx @@ -2,6 +2,7 @@ import { Label, RadioGroup } from '@/components/atoms'; import { RadioGroupItem } from '@/components/atoms/RadioGroup/RadioGroup.Item'; import { createTestId } from '@/components/organisms/Renderer'; import { useField } from '../../hooks/external'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { ICommonFieldParams, TDynamicFormField } from '../../types'; @@ -46,6 +47,7 @@ export const RadioField: TDynamicFormField = ({ element }) => ))} + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx index 7f8f23ac79..a2589eb856 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx @@ -3,6 +3,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useElement, useField } from '../../hooks/external'; +import { FieldDescription } from '../../layouts/FieldDescription'; +import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { IRadioFieldParams, RadioField } from './RadioField'; @@ -20,6 +22,14 @@ vi.mock('@/components/organisms/Renderer', () => ({ createTestId: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + +vi.mock('../../layouts/FieldErrors', () => ({ + FieldErrors: vi.fn(), +})); + describe('RadioField', () => { const mockElement = { id: 'test-radio', @@ -114,4 +124,26 @@ describe('RadioField', () => { expect(screen.getByTestId('test-radio-radio-group')).toBeInTheDocument(); expect(screen.getAllByTestId('test-radio-radio-group-item')).toHaveLength(2); }); + + it('renders FieldDescription with element prop', () => { + render(); + + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.anything(), + ); + }); + + it('renders FieldErrors with element prop', () => { + render(); + + expect(FieldErrors).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx index 8521a28548..99deaf15f7 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { useElement, useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -54,6 +55,7 @@ export const SelectField: TDynamicFormField = ({ element }) onBlur={onBlur} onFocus={onFocus} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx index bc36f4beae..a154988708 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx @@ -7,6 +7,7 @@ import { useElement, useField } from '../../hooks/external'; import { useEvents } from '../../hooks/internal/useEvents'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { TBaseFields } from '../../repositories/fields-repository'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; @@ -68,6 +69,10 @@ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: () => null, })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + describe('SelectField', () => { const mockElement = { id: 'test-id', @@ -296,4 +301,15 @@ describe('SelectField', () => { unmount(); }); + + it('should render FieldDescription with element prop', () => { + render(); + + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.any(Object), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx index 7501006fe4..ace36afc75 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx @@ -1,6 +1,7 @@ import { TagsInput } from '@/components/molecules'; import { createTestId } from '@/components/organisms/Renderer'; import { useField } from '../../hooks/external'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { ICommonFieldParams, TDynamicFormField } from '../../types'; @@ -25,6 +26,7 @@ export const TagsField: TDynamicFormField = ({ element }) => { onFocus={onFocus} disabled={disabled} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx index 55c660a213..a3689d0c0d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx @@ -3,6 +3,7 @@ import { render } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { TDeepthLevelStack } from '../../../Validator'; import { useElement, useField } from '../../hooks/external'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { TagsField } from './TagsField'; @@ -30,6 +31,10 @@ vi.mock('../FieldList/providers/StackProvider', () => ({ useStack: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + describe('TagsField', () => { const mockElement = { id: 'test-tags', @@ -111,4 +116,15 @@ describe('TagsField', () => { expect.anything(), ); }); + + it('renders FieldDescription with element prop', () => { + render(); + + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.any(Object), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx index 815e25cc41..145e7ecac3 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx @@ -5,6 +5,7 @@ import { useCallback } from 'react'; import { useElement, useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -64,6 +65,7 @@ export const TextField: TDynamicFormField = ({ element }) => { value={value?.toString() || ''} // Ensure value is string or number /> )} + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx index d15d796b27..5b08f88725 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx @@ -6,6 +6,7 @@ import { useElement, useField } from '../../hooks/external'; import { useEvents } from '../../hooks/internal/useEvents'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { ITextFieldParams, TextField } from './TextField'; @@ -65,6 +66,10 @@ vi.mock('../../hooks/internal/useUnmountEvent', () => ({ useUnmountEvent: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + describe('TextField', () => { const mockStack = [0]; const mockElement = { @@ -256,4 +261,15 @@ describe('TextField', () => { unmount(); }); + + it('renders FieldDescription with element prop', () => { + render(); + + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.any(Object), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.tsx new file mode 100644 index 0000000000..f72b0a6708 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.tsx @@ -0,0 +1,14 @@ +import { FunctionComponent } from 'react'; +import { IFormElement } from '../../types'; + +interface IFieldDescriptionProps { + element: IFormElement; +} + +export const FieldDescription: FunctionComponent = ({ element }) => { + const { description } = element.params || {}; + + if (!description) return null; + + return

{description}

; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.unit.test.tsx new file mode 100644 index 0000000000..fd175297eb --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.unit.test.tsx @@ -0,0 +1,43 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { IFormElement } from '../../types'; +import { FieldDescription } from './FieldDescription'; + +describe('FieldDescription', () => { + const mockElement = { + id: 'test-field', + params: { + description: 'Test description', + }, + } as unknown as IFormElement; + + it('should render description text when description is provided', () => { + render(); + expect(screen.getByText('Test description')).toBeInTheDocument(); + }); + + it('should apply correct styling classes', () => { + render(); + const description = screen.getByText('Test description'); + expect(description).toHaveClass('text-sm', 'text-gray-400'); + }); + + it('should not render anything when description is not provided', () => { + const elementWithoutDescription = { + id: 'test-field', + params: {}, + } as unknown as IFormElement; + + render(); + expect(screen.queryByText(/Test description/)).not.toBeInTheDocument(); + }); + + it('should not render anything when params is undefined', () => { + const elementWithoutParams = { + id: 'test-field', + } as unknown as IFormElement; + + render(); + expect(screen.queryByRole('paragraph')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/index.ts new file mode 100644 index 0000000000..1bd72d5c29 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/index.ts @@ -0,0 +1 @@ +export * from './FieldDescription'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx index 5b8d48ec1f..0cbc9c1117 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx @@ -32,7 +32,6 @@ export const FieldLayout: FunctionComponent = ({ className={ctw('flex py-2', { 'gap-2': Boolean(label), 'flex-col': layout === 'vertical', - 'flex-row flex-row-reverse items-center justify-end': layout === 'horizontal', })} >
@@ -42,13 +41,7 @@ export const FieldLayout: FunctionComponent = ({ )}
-
- {children} -
+
{children}
); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx index 7767594936..61bf8a9833 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx @@ -1,199 +1,109 @@ -import { cleanup, render, screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { IDynamicFormContext, useDynamicForm } from '../../context'; +import { useStack } from '../../fields/FieldList/providers/StackProvider'; import { useElement } from '../../hooks/external'; import { useRequired } from '../../hooks/external/useRequired'; import { IFormElement } from '../../types'; import { FieldLayout } from './FieldLayout'; -// Mock dependencies -vi.mock('@/common', () => ({ - ctw: vi.fn((base, conditionals) => { - if (conditionals) { - return Object.entries(conditionals) - .filter(([_, value]) => value) - .map(([key]) => key) - .concat(base) - .join(' '); - } - - return base; - }), -})); - -vi.mock('@/components/atoms', () => ({ - Label: ({ children, ...props }: { children: React.ReactNode; [key: string]: any }) => ( - - ), -})); - -vi.mock('../../context', () => ({ - useDynamicForm: vi.fn(() => ({ values: {} })), -})); - -vi.mock('../../fields/FieldList/providers/StackProvider', () => ({ - useStack: vi.fn(() => ({ stack: [] })), -})); - -vi.mock('../../hooks/external', () => ({ - useElement: vi.fn(element => ({ id: element.id, hidden: false })), -})); - -vi.mock('../../hooks/external/useRequired', () => ({ - useRequired: vi.fn(), -})); +vi.mock('../../context'); +vi.mock('../../fields/FieldList/providers/StackProvider'); +vi.mock('../../hooks/external'); +vi.mock('../../hooks/external/useRequired'); describe('FieldLayout', () => { - beforeEach(() => { - cleanup(); - vi.clearAllMocks(); - vi.restoreAllMocks(); - }); - const mockElement = { - id: 'test-field', + id: 'test', + type: 'text', params: { label: 'Test Label', }, - } as unknown as IFormElement; + } as unknown as IFormElement; - it('should render children', () => { + beforeEach(() => { + vi.mocked(useDynamicForm).mockReturnValue({ + values: {}, + } as unknown as IDynamicFormContext); + vi.mocked(useStack).mockReturnValue({ stack: [] }); + vi.mocked(useElement).mockReturnValue({ id: 'test-id', originId: 'test-id', hidden: false }); vi.mocked(useRequired).mockReturnValue(false); - - render( - -
Child Content
-
, - ); - - expect(screen.getByTestId('child')).toBeInTheDocument(); }); - it('should render with correct data-testid', () => { - vi.mocked(useRequired).mockReturnValue(false); - - render( - -
Child Content
-
, - ); - - expect(screen.getByTestId('test-field-field-layout')).toBeInTheDocument(); + afterEach(() => { + vi.clearAllMocks(); }); - it('should not render label when label prop is not provided', () => { - vi.mocked(useRequired).mockReturnValue(false); - - const elementWithoutLabel = { - id: 'test-field', - params: {}, - } as unknown as IFormElement; - + it('renders children and label when provided', () => { render( - -
Child Content
+ +
Test Child
, ); - expect(screen.queryByText(/Test Label/)).not.toBeInTheDocument(); + expect(screen.getByTestId('test-id-field-layout')).toBeInTheDocument(); + expect(screen.getByText('Test Label (optional)')).toBeInTheDocument(); + expect(screen.getByText('Test Child')).toBeInTheDocument(); }); - it('should render required label when field is required', () => { + it('renders required label when isRequired is true', () => { vi.mocked(useRequired).mockReturnValue(true); render( -
Child Content
+
Test Child
, ); expect(screen.getByText('Test Label')).toBeInTheDocument(); }); - it('should render optional label when field is not required', () => { - vi.mocked(useRequired).mockReturnValue(false); + it('does not render when hidden is true', () => { + vi.mocked(useElement).mockReturnValue({ id: 'test-id', originId: 'test-id', hidden: true }); render( -
Child Content
+
Test Child
, ); - expect(screen.getByText('Test Label (optional)')).toBeInTheDocument(); + expect(screen.queryByTestId('test-id-field-layout')).not.toBeInTheDocument(); }); - it('should render label with correct htmlFor attribute', () => { - vi.mocked(useRequired).mockReturnValue(false); + it('renders without label when not provided', () => { + const elementWithoutLabel = { + ...mockElement, + params: {}, + }; render( - -
Child Content
+ +
Test Child
, ); - const label = screen.getByText('Test Label (optional)'); - expect(label).toHaveAttribute('for', 'test-field'); + expect(screen.queryByRole('label')).not.toBeInTheDocument(); }); - it('should render label with correct id attribute', () => { - vi.mocked(useRequired).mockReturnValue(false); - + it('applies horizontal layout classes when specified', () => { render( - -
Child Content
+ +
Test Child
, ); - const label = screen.getByText('Test Label (optional)'); - expect(label).toHaveAttribute('id', 'test-field-label'); + const container = screen.getByTestId('test-id-field-layout').children[0]; + expect(container?.className).not.toContain('flex-col'); }); - it('should not render anything when hidden is true', () => { - vi.mocked(useElement).mockReturnValue({ id: 'test-field', hidden: true } as ReturnType< - typeof useElement - >); - vi.mocked(useRequired).mockReturnValue(false); - + it('applies vertical layout classes by default', () => { render( -
Child Content
-
, - ); - - expect(screen.queryByTestId('test-field-field-layout')).not.toBeInTheDocument(); - }); - - it('should apply correct classes for vertical layout', () => { - vi.mocked(useElement).mockReturnValue({ id: 'test-field', hidden: false } as ReturnType< - typeof useElement - >); - vi.mocked(useRequired).mockReturnValue(false); - - render( - -
Child Content
-
, - ); - - const container = screen.getByTestId('test-field-field-layout').children[0] as HTMLElement; - expect(container.className).toContain('flex-col'); - }); - - it('should apply correct classes for horizontal layout', () => { - vi.mocked(useElement).mockReturnValue({ id: 'test-field', hidden: false } as ReturnType< - typeof useElement - >); - vi.mocked(useRequired).mockReturnValue(false); - - render( - -
Child Content
+
Test Child
, ); - const container = screen.getByTestId('test-field-field-layout').children[0] as HTMLElement; - expect(container.className).toContain('flex-row'); - expect(container.className).toContain('items-center'); - expect(container.className).toContain('flex-row-reverse'); - expect(container.className).toContain('justify-end'); + const container = screen.getByTestId('test-id-field-layout').children[0]; + expect(container?.className).toContain('flex-col'); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts index 7ad64143f1..9911d4401b 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts @@ -6,6 +6,7 @@ import { IFormEventElement, TElementEvent } from '../hooks/internal/useEvents/ty export interface ICommonFieldParams { label?: string; placeholder?: string; + description?: string; } export interface IFormElement { diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts index 59186a71eb..f27d026236 100644 --- a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts @@ -423,7 +423,7 @@ describe('useValidate', () => { rerender(); - await vi.advanceTimersByTimeAsync(550); + await vi.advanceTimersByTimeAsync(600); expect(result.current.errors).toEqual([]); }); From 63020d1e9595ff1650f4c8628c835a0f396458b1 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Mon, 6 Jan 2025 13:01:52 +0200 Subject: [PATCH 34/54] feat: reworked ui elements for v2 & added tests (#2930) * feat: reworked ui elements for v2 & added tests * feat: added validate on blur * feat: added initial rendering of UI V2 * feat: updated exports * feat: implemented events provider * feat: updated exports * fix: listeners reinitialize * feat: implemented document field * feat: added document validator * fix: fixed types * fix: fixed tests & typo * fix: fixed touched issue with non field definitions in form * fix: updated validation params & cleanup --- .../Page/hooks/usePageErrors/usePageErrors.ts | 2 + .../elements/SubmitButton/helpers.ts | 14 +- .../domains/collection-flow/types/index.ts | 6 +- .../pages/CollectionFlow/CollectionFlow.tsx | 36 +--- .../CollectionFlowUI/CollectionFlowUI.tsx | 29 +++ .../form}/CountryPicker/CountryPicker.tsx | 0 .../CountryPicker/CountryPicker.unit.test.tsx | 0 .../components/form}/CountryPicker/index.ts | 0 .../form/DocumentField/DocumentField.tsx | 94 ++++++++++ .../components/form/DocumentField/helpers.ts | 61 ++++++ .../form/DocumentField/helpers.unit.test.ts | 63 +++++++ .../DocumentField/hooks/useListener/index.ts | 1 + .../hooks/useListener/useListener.ts | 21 +++ .../components/form/DocumentField/index.ts | 1 + .../components/form/DocumentField/types.ts | 13 ++ .../IndustriesPicker/IndustriesPicker.tsx | 0 .../IndustriesPicker.unit.test.tsx | 0 .../form}/IndustriesPicker/index.ts | 0 .../form}/LocalePicker/LocalePicker.tsx | 0 .../LocalePicker/LocalePicker.unit.test.tsx | 0 .../components/form}/LocalePicker/index.ts | 0 .../components/form}/MCCPicker/MCCPicker.tsx | 0 .../form}/MCCPicker/MCCPicker.unit.test.tsx | 0 .../components/form}/MCCPicker/index.ts | 0 .../NationalityPicker/NationalityPicker.tsx | 0 .../NationalityPicker.unit.test.tsx | 21 ++- .../form}/NationalityPicker/index.ts | 0 .../form}/StatePicker/StatePicker.tsx | 0 .../StatePicker/StatePicker.unit.test.tsx | 27 ++- .../components/form}/StatePicker/index.ts | 0 .../ui/ColumnElement/ColumnElement.tsx | 17 ++ .../ColumnElement/ColumnElement.unit.test.tsx | 71 +++++++ .../components/ui/ColumnElement/index.ts | 1 + .../DescriptionElement/DescriptionElement.tsx | 36 ++++ .../DescriptionElement.unit.test.tsx | 132 +++++++++++++ .../components/ui/DescriptionElement/index.ts | 1 + .../ui/DividerElement/DividerElement.tsx | 12 ++ .../DividerElement.unit.test.tsx | 55 ++++++ .../components/ui/DividerElement/index.ts | 1 + .../components/ui/H1Element/H1Element.tsx | 30 +++ .../ui/H1Element/H1Element.unit.test.tsx | 91 +++++++++ .../components/ui/H1Element/index.ts | 1 + .../components/ui/H3Element/H3Element.tsx | 30 +++ .../ui/H3Element/H3Element.unit.test.tsx | 89 +++++++++ .../components/ui/H3Element/index.ts | 1 + .../components/ui/H4Element/H4Element.tsx | 30 +++ .../ui/H4Element/H4Element.unit.test.tsx | 91 +++++++++ .../components/ui/H4Element/index.ts | 1 + .../components/ui/RowElement/RowElement.tsx | 17 ++ .../ui/RowElement/RowElement.unit.test.tsx | 71 +++++++ .../components/ui/RowElement/index.ts | 1 + .../ElementContainer/ElementContainer.tsx | 21 +++ .../ElementContainer.unit.test.tsx | 89 +++++++++ .../utility/ElementContainer/index.ts | 1 + .../organisms/CollectionFlowUI/index.ts | 1 + .../CollectionFlowUI/ui-elemenets.extends.ts | 54 ++++++ .../organisms/CollectionFlowUI/validator.ts | 4 + .../validators/document/document-validator.ts | 30 +++ .../document/document-validator.unit.test.ts | 176 ++++++++++++++++++ .../validators/document/index.ts | 2 + .../validators/document/types.ts | 5 + apps/kyb-app/tests/setup.js | 12 +- .../Form/DynamicForm/DynamicForm.tsx | 34 +++- .../DynamicForm/DynamicForm.unit.test.tsx | 42 +++++ .../ValidationShowcase/ValidationShowcase.tsx | 6 + .../Form/DynamicForm/context/types.ts | 3 +- .../SubmitButton/SubmitButton.unit.test.tsx | 3 + .../organisms/Form/DynamicForm/defaults.ts | 5 + .../DynamicForm/fields/FileField/index.ts | 1 + ...vert-form-emenents-to-validation-schema.ts | 31 ++- ...emenents-to-validation-schema.unit.test.ts | 65 ++++++- .../get-field-definitions-from-schema.ts | 26 +++ ...field-definitions-from-schema.unit.test.ts | 95 ++++++++++ .../index.ts | 1 + .../hooks/external/useField/useField.ts | 12 +- .../external/useField/useField.unit.test.ts | 32 +++- .../check-if-required/check-if-required.ts | 2 +- .../check-if-required.unit.test.ts | 2 +- .../hooks/internal/useCallbacks/index.ts | 1 - .../useCallbacks/useCallabacks.unit.test.ts | 34 ---- .../internal/useCallbacks/useCallbacks.ts | 7 - .../hooks/internal/useEvents/index.ts | 1 + .../hooks/internal/useEvents/useEvents.ts | 4 +- .../internal/useEvents/useEvents.unit.test.ts | 17 +- .../hooks/internal/useTouched/useTouched.ts | 7 +- .../useTouched/useTouched.unit.test.ts | 4 + .../useValidationSchema.ts | 7 +- .../organisms/Form/DynamicForm/index.ts | 1 + .../EventsProvider/EventsProvider.tsx | 15 ++ .../EventsProvider.unit.test.tsx | 64 +++++++ .../providers/EventsProvider/context/index.ts | 4 + .../hooks/external/useEventsConsumer/index.ts | 1 + .../useEventsConsumer/useEventsConsumer.ts | 23 +++ .../useEventsConsumer.unit.test.ts | 63 +++++++ .../external/useEventsDispatcher/index.ts | 1 + .../useEventsDispatcher.ts | 7 + .../useEventsDispatcher.unit.test.ts | 31 +++ .../hooks/internal/useEventsPool/index.ts | 1 + .../internal/useEventsPool/useEventsPool.ts | 62 ++++++ .../useEventsPool/useEventsPool.unit.test.tsx | 125 +++++++++++++ .../hooks/internal/useEventsProvider/index.ts | 1 + .../useEventsProvider/useEventsProvider.ts | 4 + .../useEventsProvider.unit.test.ts | 32 ++++ .../providers/EventsProvider/index.ts | 4 + .../providers/EventsProvider/types.ts | 15 ++ .../organisms/Form/DynamicForm/types/index.ts | 16 +- .../organisms/Form/Validator/types/index.ts | 11 +- packages/ui/src/components/organisms/index.ts | 1 + .../workflows-service/prisma/data-migrations | 2 +- 109 files changed, 2330 insertions(+), 159 deletions(-) create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/CountryPicker/CountryPicker.tsx (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/CountryPicker/CountryPicker.unit.test.tsx (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/CountryPicker/index.ts (100%) create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/DocumentField.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.unit.test.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/useListener.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/types.ts rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/IndustriesPicker/IndustriesPicker.tsx (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/IndustriesPicker/IndustriesPicker.unit.test.tsx (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/IndustriesPicker/index.ts (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/LocalePicker/LocalePicker.tsx (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/LocalePicker/LocalePicker.unit.test.tsx (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/LocalePicker/index.ts (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/MCCPicker/MCCPicker.tsx (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/MCCPicker/MCCPicker.unit.test.tsx (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/MCCPicker/index.ts (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/NationalityPicker/NationalityPicker.tsx (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/NationalityPicker/NationalityPicker.unit.test.tsx (87%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/NationalityPicker/index.ts (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/StatePicker/StatePicker.tsx (100%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/StatePicker/StatePicker.unit.test.tsx (82%) rename apps/kyb-app/src/pages/CollectionFlow/components/{form/fields => organisms/CollectionFlowUI/components/form}/StatePicker/index.ts (100%) create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/ColumnElement.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/ColumnElement.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/DescriptionElement.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/DescriptionElement.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/DividerElement.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/DividerElement.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/H1Element.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/H1Element.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/H3Element.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/H3Element.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/H4Element.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/H4Element.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/RowElement.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/RowElement.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/ElementContainer.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/ElementContainer.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/ui-elemenets.extends.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validator.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/types.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/defaults.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/get-field-definitions-from-schema.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/get-field-definitions-from-schema.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/index.ts delete mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/index.ts delete mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallabacks.unit.test.ts delete mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallbacks.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/EventsProvider.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/EventsProvider.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/context/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/useEventsConsumer.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/useEventsConsumer.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/useEventsDispatcher.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/useEventsDispatcher.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/useEventsPool.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/useEventsPool.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/useEventsProvider.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/useEventsProvider.unit.test.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/types.ts diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts b/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts index dee6560b35..0c0ae874f5 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts @@ -1,3 +1,5 @@ +// @ts-nocheck + import { ErrorField } from '@/components/organisms/DynamicUI/rule-engines'; import { findDocumentDefinitionById } from '@/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName'; import { Document, UIElement, UIPage } from '@/domains/collection-flow'; diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/SubmitButton/helpers.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/SubmitButton/helpers.ts index bfad2f76a6..77d71c1498 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/SubmitButton/helpers.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/SubmitButton/helpers.ts @@ -1,3 +1,5 @@ +// @ts-nocheck + import { ARRAY_VALUE_INDEX_PLACEHOLDER } from '@/common/consts/consts'; import { DocumentFieldParams } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField'; import { UIElement, UIPage } from '@/domains/collection-flow'; @@ -12,7 +14,7 @@ export const getElementByValueDestination = ( const findByElementDefinitionByDestination = ( targetDestination: string, - elements: UIElement[], + elements: Array>, ): UIElement | null => { for (const element of elements) { if (element.valueDestination === targetDestination) return element; @@ -22,6 +24,7 @@ export const getElementByValueDestination = ( targetDestination, element.elements, ); + if (foundElement) return foundElement; } } @@ -36,19 +39,17 @@ export const getElementByValueDestination = ( ); const element = findByElementDefinitionByDestination(originArrayDestinationPath, page.elements); + return element; } return findByElementDefinitionByDestination(destination, page.elements); }; -export const getDocumentElementByDocumentError = ( - id: string, - page: UIPage, -): UIElement | null => { +export const getDocumentElementByDocumentError = (id: string, page: any): any => { const findElement = ( id: string, - elements: UIElement[], + elements: Array>, ): UIElement | null => { for (const element of elements) { //@ts-ignore @@ -56,6 +57,7 @@ export const getDocumentElementByDocumentError = ( if (element.elements) { const foundInElements = findElement(id, element.elements); + if (foundInElements) return foundInElements; } } diff --git a/apps/kyb-app/src/domains/collection-flow/types/index.ts b/apps/kyb-app/src/domains/collection-flow/types/index.ts index f82a760962..b845067117 100644 --- a/apps/kyb-app/src/domains/collection-flow/types/index.ts +++ b/apps/kyb-app/src/domains/collection-flow/types/index.ts @@ -1,6 +1,6 @@ import { ITheme } from '@/common/types/settings'; -import { Action, Rule, UIElement } from '@/domains/collection-flow/types/ui-schema.types'; -import { AnyObject } from '@ballerine/ui'; +import { Action, Rule } from '@/domains/collection-flow/types/ui-schema.types'; +import { AnyObject, IFormElement } from '@ballerine/ui'; import { RJSFSchema, UiSchema } from '@rjsf/utils'; import { CollectionFlowConfig } from './flow-context.types'; @@ -128,7 +128,7 @@ export interface UIPage { name: string; number: number; stateName: string; - elements: Array>; + elements: Array>; actions: Action[]; pageValidation?: Rule[]; } diff --git a/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx b/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx index b4c79d42dd..6768c5cb42 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx +++ b/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx @@ -1,4 +1,3 @@ -import DOMPurify from 'dompurify'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,13 +11,7 @@ import { PageError, usePageErrors, } from '@/components/organisms/DynamicUI/Page/hooks/usePageErrors'; -import { UIRenderer } from '@/components/organisms/UIRenderer'; -import { Cell } from '@/components/organisms/UIRenderer/elements/Cell'; -import { Divider } from '@/components/organisms/UIRenderer/elements/Divider'; -import { JSONForm } from '@/components/organisms/UIRenderer/elements/JSONForm/JSONForm'; import { StepperUI } from '@/components/organisms/UIRenderer/elements/StepperUI'; -import { SubmitButton } from '@/components/organisms/UIRenderer/elements/SubmitButton'; -import { Title } from '@/components/organisms/UIRenderer/elements/Title'; import { useCustomer } from '@/components/providers/CustomerProvider'; import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types'; import { prepareInitialUIState } from '@/helpers/prepareInitialUIState'; @@ -36,30 +29,10 @@ import { setCollectionFlowStatus, setStepCompletionState, } from '@ballerine/common'; -import { AnyObject } from '@ballerine/ui'; +import { CollectionFlowUI } from './components/organisms/CollectionFlowUI'; import { FailedScreen } from './components/pages/FailedScreen'; import { useAdditionalWorkflowContext } from './hooks/useAdditionalWorkflowContext'; -const elems = { - h1: Title, - h3: (props: AnyObject) =>

{props?.options?.text}

, - h4: (props: AnyObject) =>

{props?.options?.text}

, - description: (props: AnyObject) => ( -

- ), - 'json-form': JSONForm, - container: Cell, - mainContainer: Cell, - 'submit-button': SubmitButton, - stepper: StepperUI, - divider: Divider, -}; - const isCompleted = (state: string) => state === 'completed' || state === 'finish'; const isFailed = (state: string) => state === 'failed'; @@ -145,7 +118,7 @@ export const CollectionFlow = withSessionProtected(() => { config={collectionFlowData?.config} additionalContext={additionalContext} > - {({ state, stateApi }) => { + {({ state, stateApi, payload }) => { return ( {
- +
diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx new file mode 100644 index 0000000000..8adc8d61a8 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx @@ -0,0 +1,29 @@ +import './validator'; + +import { DynamicFormV2, IFormElement } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; +import { formElementsExtends } from './ui-elemenets.extends'; + +interface ICollectionFlowUIProps { + elements: Array>; + context: object; +} + +const validationParams = { + validateOnBlur: true, + abortEarly: true, +}; + +export const CollectionFlowUI: FunctionComponent = ({ + elements, + context, +}) => { + return ( + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/CountryPicker/CountryPicker.tsx similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/CountryPicker/CountryPicker.tsx diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/CountryPicker/CountryPicker.unit.test.tsx similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/CountryPicker.unit.test.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/CountryPicker/CountryPicker.unit.test.tsx diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/CountryPicker/index.ts similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/CountryPicker/index.ts rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/CountryPicker/index.ts diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/DocumentField.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/DocumentField.tsx new file mode 100644 index 0000000000..c892649219 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/DocumentField.tsx @@ -0,0 +1,94 @@ +import { useRefValue } from '@/hooks/useRefValue'; +import { + AnyObject, + FileField, + IFormEventElement, + TDynamicFormField, + TElementEvent, + useDynamicForm, + useEventsConsumer, +} from '@ballerine/ui'; +import get from 'lodash/get'; +import { useCallback, useMemo } from 'react'; +import { buildPathToDocumentFileId, formatFileFieldElement, getDocumentIndex } from './helpers'; +import { useListener } from './hooks/useListener'; +import { IDocumentTemplate } from './types'; +// Main logic behind this component is to merge the document with the template when the document is changed +// After File input is changed, we need to merge the document with the template +// This is done by using the useListener hook to listen to the onChange event +// When the onChange event is triggered, we merge the document with the template +// If the document is being removed by the input, we remove the document from the array +// If the document is being selected by the input, we merge the document with the template + +// TODO: Tests +export const DOCUMENT_FIELD_TYPE = 'documentfield'; + +export interface IDocumentFieldParams { + documentTemplate: IDocumentTemplate; + page?: number; + pageProperty?: string; +} + +export const DocumentField: TDynamicFormField = ({ element }) => { + const { documentTemplate, page = 0, pageProperty = 'ballerineFileId' } = element.params || {}; + + if (!documentTemplate) { + console.error('Document template is required'); + throw new Error('Document template is required'); + } + + const { values, fieldHelpers } = useDynamicForm(); + const { setValue } = fieldHelpers; + + const documentIndex = useMemo(() => { + return getDocumentIndex(element.valueDestination, values, documentTemplate.id); + }, [element.valueDestination, values, documentTemplate.id]); + + const formattedElement = useMemo(() => { + return formatFileFieldElement(element, { + path: buildPathToDocumentFileId({ + rootPath: element.valueDestination, + documentIndex, + page, + pageProperty, + }), + }); + }, [element, documentIndex, page, pageProperty]); + + const valuesRef = useRefValue(values); + + const mergeDocumentWithTemplate = useCallback( + (_: TElementEvent, eventElement: IFormEventElement) => { + const documents: AnyObject[] = get(valuesRef.current, element.valueDestination, []); + + // Document is being removed by input + if (get(valuesRef.current, eventElement.valueDestination) === undefined) { + const filteredDocuments = documents.filter(document => document.id !== documentTemplate.id); + + setValue(element.id, element.valueDestination, filteredDocuments); + } + // Document selection + else { + if (!documents.length) return; + + const latestDocument = documents[documents.length - 1]; + + if (!latestDocument) return; + + const mergedDocument = { + ...((latestDocument as unknown as object) || {}), + ...documentTemplate, + }; + + documents[documents.length - 1] = mergedDocument; + + setValue(element.id, element.valueDestination, documents); + } + }, + [valuesRef, documentTemplate, element, setValue], + ); + + useEventsConsumer(useListener(element as IFormEventElement, mergeDocumentWithTemplate)); + + return ; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.ts new file mode 100644 index 0000000000..74567e7a46 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.ts @@ -0,0 +1,61 @@ +import { AnyObject, IFormElement } from '@ballerine/ui'; +import get from 'lodash/get'; + +export interface IBuildPathToDocumentFileIdParams { + rootPath: string; + documentIndex: number; + page: number; + pageProperty: string; +} + +export const buildPathToDocumentFileId = ({ + rootPath, + documentIndex, + page, + pageProperty, +}: IBuildPathToDocumentFileIdParams) => { + return `${rootPath}[${documentIndex}].pages[${page}].${pageProperty}`; +}; + +export interface IFormatFileFieldElementParams { + path: string; +} + +export const formatFileFieldElement = ( + element: IFormElement, + { path }: IFormatFileFieldElementParams, +) => { + const elementClone = structuredClone(element); + + elementClone.valueDestination = path; + + return elementClone; +}; + +export const getDocumentIndex = (path: string, context: AnyObject, documentId: string) => { + const documents = get(context, path, []); + + if (!documents.length) return 0; + + const documentIndex = documents.findIndex( + (document: { id: string }) => document.id === documentId, + ); + + return documentIndex === -1 ? documents.length : documentIndex; +}; + +export const getDocumentIndexByDocumentId = ( + path: string, + context: AnyObject, + documentId: string, +) => { + const documents = get(context, path, []); + + if (!documents.length) return 0; + + const documentIndex = documents.findIndex( + (document: { id: string }) => document.id === documentId, + ); + + return documentIndex === -1 ? 0 : documentIndex; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.unit.test.ts new file mode 100644 index 0000000000..b713b7bf1b --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/helpers.unit.test.ts @@ -0,0 +1,63 @@ +import { IFormElement } from '@ballerine/ui'; +import { buildPathToDocumentFileId, formatFileFieldElement, getDocumentIndex } from './helpers'; + +describe('buildPathToDocumentFileId', () => { + it('should build correct path with given params', () => { + const params = { + rootPath: 'documents', + documentIndex: 0, + page: 1, + pageProperty: 'fileId', + }; + + const result = buildPathToDocumentFileId(params); + + expect(result).toBe('documents[0].pages[1].fileId'); + }); +}); + +describe('formatFileFieldElement', () => { + it('should format element with new value destination', () => { + const element = { + id: 'test', + valueDestination: 'old.path', + } as IFormElement; + + const result = formatFileFieldElement(element, { path: 'new.path' }); + + expect(result).toEqual({ + id: 'test', + valueDestination: 'new.path', + }); + // Verify original wasn't mutated + expect(element.valueDestination).toBe('old.path'); + }); +}); + +describe('getDocumentIndex', () => { + it('should return 0 when documents array is empty', () => { + const result = getDocumentIndex('documents', { documents: [] }, 'doc1'); + + expect(result).toBe(0); + }); + + it('should return index when document id exists', () => { + const context = { + documents: [{ id: 'doc1' }, { id: 'doc2' }], + }; + + const result = getDocumentIndex('documents', context, 'doc2'); + + expect(result).toBe(1); + }); + + it('should return array length when document id not found', () => { + const context = { + documents: [{ id: 'doc1' }, { id: 'doc2' }], + }; + + const result = getDocumentIndex('documents', context, 'doc3'); + + expect(result).toBe(2); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/index.ts new file mode 100644 index 0000000000..4ef462d5d9 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/index.ts @@ -0,0 +1 @@ +export * from './useListener'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/useListener.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/useListener.ts new file mode 100644 index 0000000000..3ff4cd9e82 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/hooks/useListener/useListener.ts @@ -0,0 +1,21 @@ +import { IEventsListener } from '@ballerine/ui'; +import { + IFormEventElement, + TElementEvent, +} from '@ballerine/ui/dist/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types'; +import { useMemo } from 'react'; + +export const useListener = ( + element: IFormEventElement, + callback: (eventName: TElementEvent, eventElement: IFormEventElement) => void, +): IEventsListener => { + const listener: IEventsListener = useMemo(() => { + return { + id: element.id, + eventName: 'onChange', + callback, + }; + }, [element.id, callback]); + + return listener; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/index.ts new file mode 100644 index 0000000000..4aa75d4e29 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/index.ts @@ -0,0 +1 @@ +export * from './DocumentField'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/types.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/types.ts new file mode 100644 index 0000000000..00c1c2b118 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/DocumentField/types.ts @@ -0,0 +1,13 @@ +import { AnyObject } from '@ballerine/ui'; + +export interface IDocumentTemplate { + id: string; + category: string; + type: string; + issuer: { + country: string; + }; + version: string; + issuingVersion: number; + properties: AnyObject; +} diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/IndustriesPicker/IndustriesPicker.tsx similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/IndustriesPicker/IndustriesPicker.tsx diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/IndustriesPicker/IndustriesPicker.unit.test.tsx similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/IndustriesPicker.unit.test.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/IndustriesPicker/IndustriesPicker.unit.test.tsx diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/IndustriesPicker/index.ts similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/IndustriesPicker/index.ts rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/IndustriesPicker/index.ts diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/LocalePicker/LocalePicker.tsx similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/LocalePicker/LocalePicker.tsx diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/LocalePicker/LocalePicker.unit.test.tsx similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/LocalePicker.unit.test.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/LocalePicker/LocalePicker.unit.test.tsx diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/LocalePicker/index.ts similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/LocalePicker/index.ts rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/LocalePicker/index.ts diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/MCCPicker/MCCPicker.tsx similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/MCCPicker/MCCPicker.tsx diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/MCCPicker/MCCPicker.unit.test.tsx similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/MCCPicker.unit.test.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/MCCPicker/MCCPicker.unit.test.tsx diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/MCCPicker/index.ts similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/MCCPicker/index.ts rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/MCCPicker/index.ts diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/NationalityPicker/NationalityPicker.tsx similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/NationalityPicker/NationalityPicker.tsx diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/NationalityPicker/NationalityPicker.unit.test.tsx similarity index 87% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.unit.test.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/NationalityPicker/NationalityPicker.unit.test.tsx index 6545aff1f4..60f8256f74 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/NationalityPicker.unit.test.tsx +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/NationalityPicker/NationalityPicker.unit.test.tsx @@ -1,9 +1,9 @@ +import { getNationalities } from '@/helpers/countries-data'; import { IFormElement, ISelectFieldParams } from '@ballerine/ui'; import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { NATIONALITY_PICKER_FIELD_TYPE, NationalityPickerField } from './NationalityPicker'; -// Mock dependencies vi.mock('@/hooks/useLanguageParam/useLanguageParam', () => ({ useLanguageParam: () => ({ language: 'en', @@ -16,24 +16,29 @@ vi.mock('react-i18next', () => ({ }), })); -vi.mock('@/helpers/countries-data', () => ({ - getNationalities: vi.fn().mockReturnValue([ - { const: 'US', title: 'American' }, - { const: 'GB', title: 'British' }, - ]), -})); - vi.mock('@ballerine/ui', () => ({ + ...vi.importActual('@ballerine/ui'), SelectField: ({ element }: { element: any }) => (
{JSON.stringify(element)}
), })); +vi.mock('@/helpers/countries-data', () => ({ + getNationalities: vi.fn(), +})); + describe('NationalityPickerField', () => { const mockElement = { params: {}, } as IFormElement; + beforeEach(() => { + vi.mocked(getNationalities).mockReturnValue([ + { const: 'US', title: 'American' }, + { const: 'GB', title: 'British' }, + ]); + }); + it('renders SelectField with transformed nationality options', () => { render(); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/NationalityPicker/index.ts similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/NationalityPicker/index.ts rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/NationalityPicker/index.ts diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/StatePicker/StatePicker.tsx similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/StatePicker/StatePicker.tsx diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/StatePicker/StatePicker.unit.test.tsx similarity index 82% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.unit.test.tsx rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/StatePicker/StatePicker.unit.test.tsx index 0e0c2f5cf8..e21ec52953 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/StatePicker.unit.test.tsx +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/StatePicker/StatePicker.unit.test.tsx @@ -1,26 +1,20 @@ +import { getCountryStates } from '@/helpers/countries-data'; import { IFormElement, ISelectFieldParams, useDynamicForm } from '@ballerine/ui'; +import { IDynamicFormContext } from '@ballerine/ui/dist/components/organisms/Form/DynamicForm/context'; import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { STATE_PICKER_FIELD_TYPE, StatePickerField } from './StatePicker'; -// Mock dependencies vi.mock('@ballerine/ui', () => ({ ...vi.importActual('@ballerine/ui'), SelectField: ({ element }: { element: any }) => (
{JSON.stringify(element)}
), - useDynamicForm: vi.fn().mockReturnValue({ - values: { - country: 'US', - }, - }), + useDynamicForm: vi.fn(), })); vi.mock('@/helpers/countries-data', () => ({ - getCountryStates: vi.fn().mockReturnValue([ - { name: 'California', isoCode: 'CA' }, - { name: 'New York', isoCode: 'NY' }, - ]), + getCountryStates: vi.fn(), })); describe('StatePickerField', () => { @@ -30,6 +24,19 @@ describe('StatePickerField', () => { }, } as unknown as IFormElement; + beforeEach(() => { + vi.mocked(useDynamicForm).mockReturnValue({ + values: { + country: 'US', + }, + } as IDynamicFormContext); + + vi.mocked(getCountryStates).mockReturnValue([ + { name: 'California', isoCode: 'CA', countryCode: 'US' }, + { name: 'New York', isoCode: 'NY', countryCode: 'US' }, + ]); + }); + it('renders SelectField with transformed state options when country is selected', () => { render(); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/StatePicker/index.ts similarity index 100% rename from apps/kyb-app/src/pages/CollectionFlow/components/form/fields/StatePicker/index.ts rename to apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/form/StatePicker/index.ts diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/ColumnElement.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/ColumnElement.tsx new file mode 100644 index 0000000000..ffee594bd2 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/ColumnElement.tsx @@ -0,0 +1,17 @@ +import { createTestId, TDynamicFormElement } from '@ballerine/ui'; +import { ElementContainer } from '../../utility/ElementContainer'; + +export const COLUMN_UI_ELEMENT_TYPE = 'column'; + +export const ColumnElement: TDynamicFormElement = ({ + element, + children, +}) => { + return ( + +
+ {children} +
+
+ ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/ColumnElement.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/ColumnElement.unit.test.tsx new file mode 100644 index 0000000000..6ada46bce2 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/ColumnElement.unit.test.tsx @@ -0,0 +1,71 @@ +import { createTestId, IFormElement } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { vi } from 'vitest'; +import { ElementContainer } from '../../utility/ElementContainer'; +import { COLUMN_UI_ELEMENT_TYPE, ColumnElement } from './ColumnElement'; + +vi.mock('@ballerine/ui', () => ({ + createTestId: vi.fn(), +})); + +vi.mock('../../utility/ElementContainer', () => ({ + ElementContainer: vi.fn(({ children }) =>
{children}
), +})); + +describe('ColumnElement', () => { + beforeEach(() => { + vi.mocked(createTestId).mockReturnValue('test-id-column-1'); + }); + + it('renders ElementContainer', () => { + const element = { + id: 'column-1', + element: COLUMN_UI_ELEMENT_TYPE, + } as IFormElement; + + render(); + + expect(ElementContainer).toHaveBeenCalled(); + }); + + it('renders children within a column container', () => { + const element = { + id: 'column-1', + element: COLUMN_UI_ELEMENT_TYPE, + } as IFormElement; + + const childContent =
Child Content
; + + render({childContent}); + + const columnContainer = screen.getByTestId('test-id-column-1'); + expect(columnContainer).toBeInTheDocument(); + expect(columnContainer).toHaveClass('flex', 'flex-col', 'gap-2'); + expect(columnContainer).toHaveTextContent('Child Content'); + }); + + it('applies test-id to the column element', () => { + const element = { + id: 'column-1', + element: COLUMN_UI_ELEMENT_TYPE, + } as IFormElement; + + render(); + + expect(createTestId).toHaveBeenCalledWith(element); + expect(screen.getByTestId('test-id-column-1')).toBeInTheDocument(); + }); + + it('renders without children', () => { + const element = { + id: 'column-1', + element: COLUMN_UI_ELEMENT_TYPE, + } as IFormElement; + + render(); + + const columnContainer = screen.getByTestId('test-id-column-1'); + expect(columnContainer).toBeInTheDocument(); + expect(columnContainer).toBeEmptyDOMElement(); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/index.ts new file mode 100644 index 0000000000..342a25ff88 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/ColumnElement/index.ts @@ -0,0 +1 @@ +export * from './ColumnElement'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/DescriptionElement.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/DescriptionElement.tsx new file mode 100644 index 0000000000..707e61619a --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/DescriptionElement.tsx @@ -0,0 +1,36 @@ +import { createTestId, TDynamicFormElement } from '@ballerine/ui'; +import DOMPurify from 'dompurify'; +import { ElementContainer } from '../../utility/ElementContainer'; + +export const DESCRIPTION_UI_ELEMENT_TYPE = 'description'; + +export interface IDescriptionElementParams { + descriptionRaw: string; +} + +export const DescriptionElement: TDynamicFormElement< + typeof DESCRIPTION_UI_ELEMENT_TYPE, + IDescriptionElementParams +> = ({ element }) => { + const { descriptionRaw } = element.params || {}; + + if (!descriptionRaw) { + console.warn( + `${DESCRIPTION_UI_ELEMENT_TYPE} - ID:${element.id} element has no description, element will not be rendered.`, + ); + + return null; + } + + return ( + +

+ + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/DescriptionElement.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/DescriptionElement.unit.test.tsx new file mode 100644 index 0000000000..7951061820 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/DescriptionElement.unit.test.tsx @@ -0,0 +1,132 @@ +import { IFormElement, createTestId } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import DOMPurify from 'dompurify'; +import { describe, expect, it, vi } from 'vitest'; +import { ElementContainer } from '../../utility/ElementContainer'; +import { + DESCRIPTION_UI_ELEMENT_TYPE, + DescriptionElement, + IDescriptionElementParams, +} from './DescriptionElement'; + +vi.mock('@ballerine/ui', () => ({ + createTestId: vi.fn(), +})); + +vi.mock('../../utility/ElementContainer', () => ({ + ElementContainer: vi.fn(), +})); + +vi.mock('dompurify', () => ({ + default: { + sanitize: vi.fn(), + }, +})); + +describe('DescriptionElement', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(ElementContainer).mockImplementation(({ children }) => children); + vi.mocked(DOMPurify.sanitize).mockImplementation(str => str as string); + vi.mocked(createTestId).mockImplementation(element => `test-id-${element.id}`); + }); + + it('renders within ElementContainer', () => { + const element = { + element: DESCRIPTION_UI_ELEMENT_TYPE, + params: { + descriptionRaw: 'Test Description', + }, + } as IFormElement; + + render(); + + expect(ElementContainer).toHaveBeenCalled(); + }); + + it('renders description text correctly', () => { + const element = { + element: DESCRIPTION_UI_ELEMENT_TYPE, + params: { + descriptionRaw: 'Test Description', + }, + } as IFormElement; + + render(); + + expect(screen.getByText('Test Description')).toBeInTheDocument(); + expect(screen.getByText('Test Description')).toHaveClass( + 'font-inter pb-2 text-sm text-slate-500', + ); + }); + + it('sanitizes HTML content', () => { + const element = { + element: DESCRIPTION_UI_ELEMENT_TYPE, + params: { + descriptionRaw: '

Test Description

', + }, + } as IFormElement; + + render(); + + expect(DOMPurify.sanitize).toHaveBeenCalledWith('

Test Description

'); + }); + + it('does not render when descriptionRaw is empty', () => { + const element = { + element: DESCRIPTION_UI_ELEMENT_TYPE, + params: { + descriptionRaw: '', + }, + } as IFormElement; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('does not render when params is undefined', () => { + const element = { + element: DESCRIPTION_UI_ELEMENT_TYPE, + params: undefined, + } as IFormElement; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('logs warning when descriptionRaw is empty', () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => null); + const element = { + id: 'test-id', + element: DESCRIPTION_UI_ELEMENT_TYPE, + params: { + descriptionRaw: '', + }, + } as IFormElement; + + render(); + + expect(consoleSpy).toHaveBeenCalledWith( + 'description - ID:test-id element has no description, element will not be rendered.', + ); + consoleSpy.mockRestore(); + }); + + it('applies test-id to the description element', () => { + const element = { + id: 'description-1', + element: DESCRIPTION_UI_ELEMENT_TYPE, + params: { + descriptionRaw: 'Test Description', + }, + } as IFormElement; + + render(); + + expect(createTestId).toHaveBeenCalledWith(element); + expect(screen.getByTestId('test-id-description-1')).toBeInTheDocument(); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/index.ts new file mode 100644 index 0000000000..3f1c8a4ad4 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DescriptionElement/index.ts @@ -0,0 +1 @@ +export * from './DescriptionElement'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/DividerElement.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/DividerElement.tsx new file mode 100644 index 0000000000..9d625fdb06 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/DividerElement.tsx @@ -0,0 +1,12 @@ +import { createTestId, TDynamicFormElement } from '@ballerine/ui'; +import { ElementContainer } from '../../utility/ElementContainer'; + +export const DIVIDER_UI_ELEMENT_TYPE = 'divider'; + +export const DividerElement: TDynamicFormElement = ({ + element, +}) => ( + +
+ +); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/DividerElement.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/DividerElement.unit.test.tsx new file mode 100644 index 0000000000..e5deac5930 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/DividerElement.unit.test.tsx @@ -0,0 +1,55 @@ +import { IFormElement, createTestId } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ElementContainer } from '../../utility/ElementContainer'; +import { DIVIDER_UI_ELEMENT_TYPE, DividerElement } from './DividerElement'; + +vi.mock('@ballerine/ui', () => ({ + createTestId: vi.fn(), +})); + +vi.mock('../../utility/ElementContainer', () => ({ + ElementContainer: vi.fn(), +})); + +describe('DividerElement', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(ElementContainer).mockImplementation(({ children }) => children); + vi.mocked(createTestId).mockImplementation(element => `test-id-${element.id}`); + }); + + it('renders within ElementContainer', () => { + const element = { + element: DIVIDER_UI_ELEMENT_TYPE, + } as IFormElement; + + render(); + + expect(ElementContainer).toHaveBeenCalled(); + }); + + it('renders divider with correct styling', () => { + const element = { + element: DIVIDER_UI_ELEMENT_TYPE, + } as IFormElement; + + render(); + + const divider = screen.getByTestId('test-id-undefined'); + expect(divider).toBeInTheDocument(); + expect(divider).toHaveClass('my-3', 'h-[1px]', 'w-full', 'bg-[#CECECE]'); + }); + + it('applies test-id to the divider element', () => { + const element = { + id: 'divider-1', + element: DIVIDER_UI_ELEMENT_TYPE, + } as IFormElement; + + render(); + + expect(createTestId).toHaveBeenCalledWith(element); + expect(screen.getByTestId('test-id-divider-1')).toBeInTheDocument(); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/index.ts new file mode 100644 index 0000000000..e939ede0b3 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/DividerElement/index.ts @@ -0,0 +1 @@ +export * from './DividerElement'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/H1Element.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/H1Element.tsx new file mode 100644 index 0000000000..7729823ba4 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/H1Element.tsx @@ -0,0 +1,30 @@ +import { createTestId, TDynamicFormElement } from '@ballerine/ui'; +import { ElementContainer } from '../../utility/ElementContainer'; + +export const H1_UI_ELEMENT_TYPE = 'h1'; + +export interface IH1ElementParams { + text: string; +} + +export const H1Element: TDynamicFormElement = ({ + element, +}) => { + const { text = '' } = element.params || {}; + + if (!text) { + console.warn( + `${H1_UI_ELEMENT_TYPE} - ID:${element.id} element has no text, element will not be rendered.`, + ); + + return null; + } + + return ( + +

+ {text} +

+
+ ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/H1Element.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/H1Element.unit.test.tsx new file mode 100644 index 0000000000..5bec00f80e --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/H1Element.unit.test.tsx @@ -0,0 +1,91 @@ +import { createTestId, IFormElement } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ElementContainer } from '../../utility/ElementContainer'; +import { H1_UI_ELEMENT_TYPE, H1Element, IH1ElementParams } from './H1Element'; + +vi.mock('@ballerine/ui', () => ({ + createTestId: vi.fn(), +})); + +vi.mock('../../utility/ElementContainer', () => ({ + ElementContainer: vi.fn(), +})); + +describe('H1Element', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(createTestId).mockReturnValue('test-id'); + vi.mocked(ElementContainer).mockImplementation(({ children }) => children); + }); + + it('renders within ElementContainer', () => { + const element = { + element: H1_UI_ELEMENT_TYPE, + params: { + text: 'Test Heading', + }, + } as IFormElement; + + render(); + + expect(ElementContainer).toHaveBeenCalled(); + }); + + it('renders heading text correctly', () => { + const element = { + element: H1_UI_ELEMENT_TYPE, + params: { + text: 'Test Heading', + }, + } as IFormElement; + + render(); + + expect(screen.getByText('Test Heading')).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 1 })).toHaveClass('pb-6 pt-4 text-3xl font-bold'); + expect(screen.getByTestId('test-id')).toBeInTheDocument(); + }); + + it('does not render when text is empty', () => { + const element = { + element: H1_UI_ELEMENT_TYPE, + params: { + text: '', + }, + } as IFormElement; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('does not render when params is undefined', () => { + const element = { + element: H1_UI_ELEMENT_TYPE, + params: undefined, + } as IFormElement; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('logs warning when text is empty', () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => null); + const element = { + id: 'test-id', + element: H1_UI_ELEMENT_TYPE, + params: { + text: '', + }, + } as IFormElement; + + render(); + + expect(consoleSpy).toHaveBeenCalledWith( + 'h1 - ID:test-id element has no text, element will not be rendered.', + ); + consoleSpy.mockRestore(); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/index.ts new file mode 100644 index 0000000000..f109698a08 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H1Element/index.ts @@ -0,0 +1 @@ +export * from './H1Element'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/H3Element.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/H3Element.tsx new file mode 100644 index 0000000000..1918b2977b --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/H3Element.tsx @@ -0,0 +1,30 @@ +import { createTestId, TDynamicFormElement } from '@ballerine/ui'; +import { ElementContainer } from '../../utility/ElementContainer'; + +export const H3_UI_ELEMENT_TYPE = 'h3'; + +export interface IH3ElementParams { + text: string; +} + +export const H3Element: TDynamicFormElement = ({ + element, +}) => { + const { text = '' } = element.params || {}; + + if (!text) { + console.warn( + `${H3_UI_ELEMENT_TYPE} - ID:${element.id} element has no text, element will not be rendered.`, + ); + + return null; + } + + return ( + +

+ {text} +

+
+ ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/H3Element.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/H3Element.unit.test.tsx new file mode 100644 index 0000000000..1214e46d70 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/H3Element.unit.test.tsx @@ -0,0 +1,89 @@ +import { createTestId, IFormElement } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ElementContainer } from '../../utility/ElementContainer'; +import { H3_UI_ELEMENT_TYPE, H3Element, IH3ElementParams } from './H3Element'; + +vi.mock('@ballerine/ui', () => ({ + createTestId: vi.fn(), +})); + +vi.mock('../../utility/ElementContainer', () => ({ + ElementContainer: vi.fn(), +})); + +describe('H3Element', () => { + beforeEach(() => { + vi.mocked(createTestId).mockReturnValue('test-id'); + vi.mocked(ElementContainer).mockImplementation(({ children }) => children); + }); + + it('renders within ElementContainer', () => { + const element = { + element: H3_UI_ELEMENT_TYPE, + params: { + text: 'Test Heading', + }, + } as IFormElement; + + render(); + expect(ElementContainer).toHaveBeenCalled(); + }); + + it('renders heading text correctly', () => { + const element = { + element: H3_UI_ELEMENT_TYPE, + params: { + text: 'Test Heading', + }, + } as IFormElement; + + render(); + + expect(screen.getByText('Test Heading')).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 3 })).toHaveClass('pt-4 text-xl font-bold'); + expect(screen.getByTestId('test-id')).toBeInTheDocument(); + }); + + it('does not render when text is empty', () => { + const element = { + element: H3_UI_ELEMENT_TYPE, + params: { + text: '', + }, + } as IFormElement; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('does not render when params is undefined', () => { + const element = { + element: H3_UI_ELEMENT_TYPE, + params: undefined, + } as IFormElement; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('logs warning when text is empty', () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => null); + const element = { + id: 'test-id', + element: H3_UI_ELEMENT_TYPE, + params: { + text: '', + }, + } as IFormElement; + + render(); + + expect(consoleSpy).toHaveBeenCalledWith( + 'h3 - ID:test-id element has no text, element will not be rendered.', + ); + consoleSpy.mockRestore(); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/index.ts new file mode 100644 index 0000000000..3b240dd36b --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H3Element/index.ts @@ -0,0 +1 @@ +export * from './H3Element'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/H4Element.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/H4Element.tsx new file mode 100644 index 0000000000..9d65940e56 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/H4Element.tsx @@ -0,0 +1,30 @@ +import { createTestId, TDynamicFormElement } from '@ballerine/ui'; +import { ElementContainer } from '../../utility/ElementContainer'; + +export const H4_UI_ELEMENT_TYPE = 'h4'; + +export interface IH4ElementParams { + text: string; +} + +export const H4Element: TDynamicFormElement = ({ + element, +}) => { + const { text } = element.params || {}; + + if (!text) { + console.warn( + `${H4_UI_ELEMENT_TYPE} - ID:${element.id} element has no text, element will not be rendered.`, + ); + + return null; + } + + return ( + +

+ {text} +

+
+ ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/H4Element.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/H4Element.unit.test.tsx new file mode 100644 index 0000000000..0082b25e4c --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/H4Element.unit.test.tsx @@ -0,0 +1,91 @@ +import { createTestId, IFormElement } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ElementContainer } from '../../utility/ElementContainer'; +import { H4_UI_ELEMENT_TYPE, H4Element, IH4ElementParams } from './H4Element'; + +vi.mock('@ballerine/ui', () => ({ + createTestId: vi.fn(), +})); + +vi.mock('../../utility/ElementContainer', () => ({ + ElementContainer: vi.fn(), +})); + +describe('H4Element', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(createTestId).mockReturnValue('test-id'); + vi.mocked(ElementContainer).mockImplementation(({ children }) => children); + }); + + it('renders within ElementContainer', () => { + const element = { + element: H4_UI_ELEMENT_TYPE, + params: { + text: 'Test Heading', + }, + } as IFormElement; + + render(); + + expect(ElementContainer).toHaveBeenCalled(); + }); + + it('renders heading text correctly', () => { + const element = { + element: H4_UI_ELEMENT_TYPE, + params: { + text: 'Test Heading', + }, + } as IFormElement; + + render(); + + expect(screen.getByText('Test Heading')).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 4 })).toHaveClass('pb-3 text-base font-bold'); + expect(screen.getByTestId('test-id')).toBeInTheDocument(); + }); + + it('does not render when text is empty', () => { + const element = { + element: H4_UI_ELEMENT_TYPE, + params: { + text: '', + }, + } as IFormElement; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('does not render when params is undefined', () => { + const element = { + element: H4_UI_ELEMENT_TYPE, + params: undefined, + } as IFormElement; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('logs warning when text is empty', () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => null); + const element = { + id: 'test-id', + element: H4_UI_ELEMENT_TYPE, + params: { + text: '', + }, + } as IFormElement; + + render(); + + expect(consoleSpy).toHaveBeenCalledWith( + 'h4 - ID:test-id element has no text, element will not be rendered.', + ); + consoleSpy.mockRestore(); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/index.ts new file mode 100644 index 0000000000..5c7048d77b --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/H4Element/index.ts @@ -0,0 +1 @@ +export * from './H4Element'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/RowElement.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/RowElement.tsx new file mode 100644 index 0000000000..ab05cc7a65 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/RowElement.tsx @@ -0,0 +1,17 @@ +import { createTestId, TDynamicFormElement } from '@ballerine/ui'; +import { ElementContainer } from '../../utility/ElementContainer'; + +export const ROW_UI_ELEMENT_TYPE = 'row'; + +export const RowElement: TDynamicFormElement = ({ + element, + children, +}) => { + return ( + +
+ {children} +
+
+ ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/RowElement.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/RowElement.unit.test.tsx new file mode 100644 index 0000000000..aefdb708b3 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/RowElement.unit.test.tsx @@ -0,0 +1,71 @@ +import { createTestId, IFormElement } from '@ballerine/ui'; +import { render, screen } from '@testing-library/react'; +import { vi } from 'vitest'; +import { ElementContainer } from '../../utility/ElementContainer'; +import { ROW_UI_ELEMENT_TYPE, RowElement } from './RowElement'; + +vi.mock('@ballerine/ui', () => ({ + createTestId: vi.fn(), +})); + +vi.mock('../../utility/ElementContainer', () => ({ + ElementContainer: vi.fn(({ children }) =>
{children}
), +})); + +describe('RowElement', () => { + beforeEach(() => { + vi.mocked(createTestId).mockReturnValue('test-id-row-1'); + }); + + it('renders ElementContainer', () => { + const element = { + id: 'row-1', + element: ROW_UI_ELEMENT_TYPE, + } as IFormElement; + + render(); + + expect(ElementContainer).toHaveBeenCalled(); + }); + + it('renders children within a row container', () => { + const element = { + id: 'row-1', + element: ROW_UI_ELEMENT_TYPE, + } as IFormElement; + + const childContent =
Child Content
; + + render({childContent}); + + const rowContainer = screen.getByTestId('test-id-row-1'); + expect(rowContainer).toBeInTheDocument(); + expect(rowContainer).toHaveClass('flex', 'flex-row', 'gap-2'); + expect(rowContainer).toHaveTextContent('Child Content'); + }); + + it('applies test-id to the row element', () => { + const element = { + id: 'row-1', + element: ROW_UI_ELEMENT_TYPE, + } as IFormElement; + + render(); + + expect(createTestId).toHaveBeenCalledWith(element); + expect(screen.getByTestId('test-id-row-1')).toBeInTheDocument(); + }); + + it('renders without children', () => { + const element = { + id: 'row-1', + element: ROW_UI_ELEMENT_TYPE, + } as IFormElement; + + render(); + + const rowContainer = screen.getByTestId('test-id-row-1'); + expect(rowContainer).toBeInTheDocument(); + expect(rowContainer).toBeEmptyDOMElement(); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/index.ts new file mode 100644 index 0000000000..9e00f26b60 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/ui/RowElement/index.ts @@ -0,0 +1 @@ +export * from './RowElement'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/ElementContainer.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/ElementContainer.tsx new file mode 100644 index 0000000000..297c6437c0 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/ElementContainer.tsx @@ -0,0 +1,21 @@ +import { IFormElement, TDeepthLevelStack, useElement } from '@ballerine/ui'; + +interface IElementContainerProps { + element: IFormElement; + stack?: TDeepthLevelStack; + children: React.ReactNode; +} + +export const ElementContainer: React.FC = ({ + children, + element, + stack, +}) => { + const { hidden } = useElement(element, stack); + + if (hidden) { + return null; + } + + return <>{children}; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/ElementContainer.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/ElementContainer.unit.test.tsx new file mode 100644 index 0000000000..f1fbd84991 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/ElementContainer.unit.test.tsx @@ -0,0 +1,89 @@ +import { IFormElement, TDeepthLevelStack, useElement } from '@ballerine/ui'; +import { render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ElementContainer } from './ElementContainer'; + +vi.mock('@ballerine/ui', () => ({ + useElement: vi.fn(), +})); + +describe('ElementContainer', () => { + const mockElement = { + id: 'test-id', + element: 'test', + } as IFormElement; + + const mockStack = {} as TDeepthLevelStack; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders children when not hidden', () => { + vi.mocked(useElement).mockReturnValue({ + hidden: false, + id: 'test-id', + originId: 'test-origin-id', + }); + + const { container } = render( + +
Test Content
+
, + ); + + expect(container).toHaveTextContent('Test Content'); + expect(useElement).toHaveBeenCalledWith(mockElement, mockStack); + }); + + it('does not render children when hidden', () => { + vi.mocked(useElement).mockReturnValue({ + hidden: true, + id: 'test-id', + originId: 'test-origin-id', + }); + + const { container } = render( + +
Test Content
+
, + ); + + expect(container).toBeEmptyDOMElement(); + expect(useElement).toHaveBeenCalledWith(mockElement, mockStack); + }); + + it('calls useElement with correct props', () => { + vi.mocked(useElement).mockReturnValue({ + hidden: false, + id: 'test-id', + originId: 'test-origin-id', + }); + + render( + +
Test Content
+
, + ); + + expect(useElement).toHaveBeenCalledWith(mockElement, mockStack); + expect(useElement).toHaveBeenCalledTimes(1); + }); + + it('works without stack prop', () => { + vi.mocked(useElement).mockReturnValue({ + hidden: false, + id: 'test-id', + originId: 'test-origin-id', + }); + + const { container } = render( + +
Test Content
+
, + ); + + expect(container).toHaveTextContent('Test Content'); + expect(useElement).toHaveBeenCalledWith(mockElement, undefined); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/index.ts new file mode 100644 index 0000000000..8a9adeb013 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/ElementContainer/index.ts @@ -0,0 +1 @@ +export * from './ElementContainer'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/index.ts new file mode 100644 index 0000000000..5b74529938 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/index.ts @@ -0,0 +1 @@ +export * from './CollectionFlowUI'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/ui-elemenets.extends.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/ui-elemenets.extends.ts new file mode 100644 index 0000000000..66308d5990 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/ui-elemenets.extends.ts @@ -0,0 +1,54 @@ +import { TBaseFields } from '@ballerine/ui'; +import { COUNTRY_PICKER_FIELD_TYPE, CountryPickerField } from './components/form/CountryPicker'; +import { DOCUMENT_FIELD_TYPE } from './components/form/DocumentField'; +import { DocumentField } from './components/form/DocumentField/DocumentField'; +import { + INDUSTRIES_PICKER_FIELD_TYPE, + IndustriesPickerField, +} from './components/form/IndustriesPicker'; +import { LOCALE_PICKER_FIELD_TYPE, LocalePickerField } from './components/form/LocalePicker'; +import { MCC_PICKER_FIELD_TYPE, MCCPickerField } from './components/form/MCCPicker'; +import { + NATIONALITY_PICKER_FIELD_TYPE, + NationalityPickerField, +} from './components/form/NationalityPicker'; +import { STATE_PICKER_FIELD_TYPE, StatePickerField } from './components/form/StatePicker'; +import { COLUMN_UI_ELEMENT_TYPE, ColumnElement } from './components/ui/ColumnElement'; +import { + DESCRIPTION_UI_ELEMENT_TYPE, + DescriptionElement, +} from './components/ui/DescriptionElement'; +import { DIVIDER_UI_ELEMENT_TYPE, DividerElement } from './components/ui/DividerElement'; +import { H1_UI_ELEMENT_TYPE, H1Element } from './components/ui/H1Element'; +import { H3_UI_ELEMENT_TYPE, H3Element } from './components/ui/H3Element'; +import { H4_UI_ELEMENT_TYPE, H4Element } from './components/ui/H4Element'; +import { ROW_UI_ELEMENT_TYPE, RowElement } from './components/ui/RowElement'; + +const fields = { + [COUNTRY_PICKER_FIELD_TYPE]: CountryPickerField, + [INDUSTRIES_PICKER_FIELD_TYPE]: IndustriesPickerField, + [LOCALE_PICKER_FIELD_TYPE]: LocalePickerField, + [MCC_PICKER_FIELD_TYPE]: MCCPickerField, + [NATIONALITY_PICKER_FIELD_TYPE]: NationalityPickerField, + [STATE_PICKER_FIELD_TYPE]: StatePickerField, + [DOCUMENT_FIELD_TYPE]: DocumentField, +}; + +const uiElements = { + [H1_UI_ELEMENT_TYPE]: H1Element, + [H3_UI_ELEMENT_TYPE]: H3Element, + [H4_UI_ELEMENT_TYPE]: H4Element, + [DESCRIPTION_UI_ELEMENT_TYPE]: DescriptionElement, + [DIVIDER_UI_ELEMENT_TYPE]: DividerElement, + [COLUMN_UI_ELEMENT_TYPE]: ColumnElement, + [ROW_UI_ELEMENT_TYPE]: RowElement, +}; + +export const formElementsExtends = { + ...fields, + ...uiElements, +}; + +export type TCollectionFlowElements = keyof typeof formElementsExtends; + +export type TElements = TCollectionFlowElements | TBaseFields; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validator.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validator.ts new file mode 100644 index 0000000000..4b50a549e2 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validator.ts @@ -0,0 +1,4 @@ +import { registerValidator } from '@ballerine/ui'; +import { documentValidator } from './validators/document'; + +registerValidator('document', documentValidator); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts new file mode 100644 index 0000000000..210ddce399 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts @@ -0,0 +1,30 @@ +import { TDocument } from '@ballerine/common'; +import { TBaseValidators, TValidator } from '@ballerine/ui'; +import { IDocumentValidatorParams } from './types'; + +export const documentValidator: TValidator< + TDocument[], + IDocumentValidatorParams, + TBaseValidators | 'document' +> = (value, params) => { + const { message = 'Document is required' } = params; + const { id, pageNumber = 0, pageProperty = 'ballerineFileId' } = params.value; + + if (!Array.isArray(value) || !value.length) { + throw new Error('Document is required'); + } + + const document = value.find(doc => doc.id === id); + + if (!document) { + throw new Error(message); + } + + const documentValue = document[pageNumber]?.[pageProperty]; + + if (!documentValue) { + throw new Error(message); + } + + return true; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts new file mode 100644 index 0000000000..1953d082ef --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts @@ -0,0 +1,176 @@ +import { TDocument } from '@ballerine/common'; +import { ICommonValidator, TBaseValidators } from '@ballerine/ui'; +import { documentValidator } from './document-validator'; +import { IDocumentValidatorParams } from './types'; + +describe('documentValidator', () => { + const mockParams: ICommonValidator = { + value: { + id: 'test-id', + pageNumber: 0, + pageProperty: 'ballerineFileId', + }, + message: 'Custom error message', + type: 'document', + }; + + it('should return true for valid document', () => { + const mockDocuments = [ + { + id: 'test-id', + 0: { + ballerineFileId: 'file-123', + }, + propertiesSchema: {}, + }, + ] as TDocument[]; + + expect(documentValidator(mockDocuments, mockParams)).toBe(true); + }); + + it('should throw error if value is not an array', () => { + expect(() => { + documentValidator(null as any, mockParams); + }).toThrow('Document is required'); + }); + + it('should throw error if value is an empty array', () => { + expect(() => { + documentValidator([], mockParams); + }).toThrow('Document is required'); + }); + + it('should throw error if document with specified id is not found', () => { + const mockDocuments = [ + { + id: 'wrong-id', + 0: { + ballerineFileId: 'file-123', + }, + propertiesSchema: {}, + }, + ] as TDocument[]; + + expect(() => { + documentValidator(mockDocuments, mockParams); + }).toThrow('Custom error message'); + }); + + it('should throw error if document page property is not found', () => { + const mockDocuments = [ + { + id: 'test-id', + 0: {}, + propertiesSchema: {}, + }, + ] as TDocument[]; + + expect(() => { + documentValidator(mockDocuments, mockParams); + }).toThrow('Custom error message'); + }); + + it('should use default message if not provided', () => { + const paramsWithoutMessage: ICommonValidator< + IDocumentValidatorParams, + TBaseValidators | 'document' + > = { + value: { + id: 'test-id', + pageNumber: 0, + pageProperty: 'ballerineFileId', + }, + type: 'document', + }; + + const mockDocuments = [ + { + id: 'wrong-id', + 0: { + ballerineFileId: 'file-123', + }, + propertiesSchema: {}, + }, + ] as TDocument[]; + + expect(() => { + documentValidator(mockDocuments, paramsWithoutMessage); + }).toThrow('Document is required'); + }); + + it('should use default pageNumber if not provided', () => { + const paramsWithoutPageNumber: ICommonValidator< + IDocumentValidatorParams, + TBaseValidators | 'document' + > = { + value: { + id: 'test-id', + pageProperty: 'ballerineFileId', + }, + message: 'Custom error message', + type: 'document', + }; + + const mockDocuments = [ + { + id: 'test-id', + 0: { + ballerineFileId: 'file-123', + }, + propertiesSchema: {}, + }, + ] as TDocument[]; + + expect(documentValidator(mockDocuments, paramsWithoutPageNumber)).toBe(true); + }); + + it('should use default pageProperty if not provided', () => { + const paramsWithoutPageProperty: ICommonValidator< + IDocumentValidatorParams, + TBaseValidators | 'document' + > = { + value: { + id: 'test-id', + pageNumber: 0, + }, + message: 'Custom error message', + type: 'document', + }; + + const mockDocuments = [ + { + id: 'test-id', + 0: { + ballerineFileId: 'file-123', + }, + propertiesSchema: {}, + }, + ] as TDocument[]; + + expect(documentValidator(mockDocuments, paramsWithoutPageProperty)).toBe(true); + }); + + it('should not throw when document value is a File object', () => { + const params: ICommonValidator = { + value: { + id: 'test-id', + pageNumber: 0, + pageProperty: 'ballerineFileId', + }, + message: 'Custom error message', + type: 'document', + }; + + const mockDocuments = [ + { + id: 'test-id', + 0: { + ballerineFileId: new File([''], 'test.jpg', { type: 'image/jpeg' }), + }, + propertiesSchema: {}, + }, + ] as TDocument[]; + + expect(documentValidator(mockDocuments, params)).toBe(true); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/index.ts new file mode 100644 index 0000000000..7fd5cef0c2 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/index.ts @@ -0,0 +1,2 @@ +export * from './document-validator'; +export * from './types'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/types.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/types.ts new file mode 100644 index 0000000000..90ae5419d2 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/types.ts @@ -0,0 +1,5 @@ +export interface IDocumentValidatorParams { + id: string; + pageNumber?: number; + pageProperty?: string; +} diff --git a/apps/kyb-app/tests/setup.js b/apps/kyb-app/tests/setup.js index 20bfcac3a4..135784f368 100644 --- a/apps/kyb-app/tests/setup.js +++ b/apps/kyb-app/tests/setup.js @@ -1,11 +1,19 @@ import '@testing-library/jest-dom'; -import '@testing-library/jest-dom/vitest'; +import matchers from '@testing-library/jest-dom/matchers'; import { cleanup } from '@testing-library/react'; -import { afterEach, vi } from 'vitest'; +import { afterEach, expect, vi } from 'vitest'; + +if (matchers) { + // Extend Vitest's expect with jest-dom matchers + expect.extend(matchers); +} // runs a cleanup after each test case (e.g. clearing jsdom) afterEach(() => { cleanup(); + vi.clearAllMocks(); + vi.resetAllMocks(); + vi.restoreAllMocks(); }); global.jest = vi; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx index bb22dad93c..e2c253f1c9 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -3,11 +3,13 @@ import { FunctionComponent, useMemo } from 'react'; import { Renderer, TRendererSchema } from '../../Renderer'; import { ValidatorProvider } from '../Validator'; import { DynamicFormContext, IDynamicFormContext } from './context'; +import { defaultValidationParams } from './defaults'; import { useSubmit } from './hooks/external/useSubmit'; import { useFieldHelpers } from './hooks/internal/useFieldHelpers'; import { useTouched } from './hooks/internal/useTouched'; import { useValidationSchema } from './hooks/internal/useValidationSchema'; import { useValues } from './hooks/internal/useValues'; +import { EventsProvider } from './providers/EventsProvider'; import { TaskRunner } from './providers/TaskRunner'; import { extendFieldsRepository, getFieldsRepository } from './repositories'; import { IDynamicFormProps } from './types'; @@ -15,7 +17,7 @@ import { IDynamicFormProps } from './types'; export const DynamicFormV2: FunctionComponent = ({ elements, values: initialValues, - validationParams, + validationParams = defaultValidationParams, fieldExtends, metadata, onChange, @@ -44,20 +46,32 @@ export const DynamicFormV2: FunctionComponent = ({ onEvent, }, metadata: metadata ?? {}, + validationParams: validationParams ?? {}, }), - [touchedApi.touched, valuesApi.values, submit, fieldHelpers, fieldExtends, onEvent, metadata], + [ + touchedApi.touched, + valuesApi.values, + submit, + fieldHelpers, + fieldExtends, + onEvent, + metadata, + validationParams, + ], ); return ( - - - - - + + + + + + + ); }; 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 6ebd5cc7b8..15eda1c5c1 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 @@ -9,6 +9,7 @@ import { useFieldHelpers } from './hooks/internal/useFieldHelpers'; import { useTouched } from './hooks/internal/useTouched'; import { useValidationSchema } from './hooks/internal/useValidationSchema'; import { useValues } from './hooks/internal/useValues'; +import { EventsProvider } from './providers/EventsProvider'; import { TaskRunner } from './providers/TaskRunner'; import { ICommonFieldParams, IDynamicFormProps, IFormElement } from './types'; @@ -29,6 +30,8 @@ vi.mock('./hooks/internal/useValues'); vi.mock('./providers/TaskRunner'); +vi.mock('./providers/EventsProvider'); + vi.mock('./context', () => ({ DynamicFormContext: { Provider: vi.fn(({ children, value }: any) => { @@ -51,6 +54,9 @@ describe('DynamicFormV2', () => { vi.mocked(TaskRunner).mockImplementation(({ children }: any) => { return
{children}
; }); + vi.mocked(EventsProvider).mockImplementation(({ children }: any) => { + return
{children}
; + }); vi.mocked(useTouched).mockReturnValue({ touched: {}, @@ -97,6 +103,16 @@ describe('DynamicFormV2', () => { expect(getByTestId('task-runner')).toBeInTheDocument(); }); + it('should render EventsProvider with correct props', () => { + render(); + expect(EventsProvider).toHaveBeenCalledWith( + expect.objectContaining({ + onEvent: mockProps.onEvent, + }), + expect.anything(), + ); + }); + it('should pass elements to useValidationSchema', () => { const elements = [{ id: 'test', element: 'textfield' }] as unknown as Array< IFormElement @@ -180,6 +196,32 @@ describe('DynamicFormV2', () => { onEvent: mockProps.onEvent, }, metadata: mockProps.metadata, + validationParams: mockProps.validationParams, }); }); + + it('should use default validation params when not provided in props', () => { + const propsWithoutValidation = { ...mockProps }; + delete propsWithoutValidation.validationParams; + + render(); + + const providerProps = vi.mocked(DynamicFormContext.Provider).mock.calls[0]?.[0]; + + expect(providerProps?.value.validationParams).toEqual({ + validateOnBlur: true, + }); + }); + + it('should use validation params from props when provided', () => { + const customValidationParams = { + validateOnBlur: false, + }; + + render(); + + const providerProps = vi.mocked(DynamicFormContext.Provider).mock.calls[0]?.[0]; + + expect(providerProps?.value.validationParams).toEqual(customValidationParams); + }); }); 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 e0cf5e3095..d62e52f60e 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 @@ -2,8 +2,13 @@ import { AnyObject } from '@/common'; import { useState } from 'react'; import { JSONEditorComponent } from '../../../Validator/_stories/components/JsonEditor/JsonEditor'; import { DynamicFormV2 } from '../../DynamicForm'; +import { IDynamicFormValidationParams } from '../../types'; import { schema } from './schema'; +const validationParams: IDynamicFormValidationParams = { + validateOnBlur: false, +}; + export const ValidationShowcaseComponent = () => { const [context, setContext] = useState({}); @@ -17,6 +22,7 @@ export const ValidationShowcaseComponent = () => { console.log('onSubmit'); }} onChange={setContext} + validationParams={validationParams} // onEvent={console.log} />
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 af488d780e..e646a1c3f6 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/context/types.ts @@ -1,7 +1,7 @@ import { IFormEventElement, TElementEvent } from '../hooks/internal/useEvents/types'; import { IFieldHelpers } from '../hooks/internal/useFieldHelpers/types'; import { ITouchedState } from '../hooks/internal/useTouched'; -import { TElementsMap } from '../types'; +import { IDynamicFormValidationParams, TElementsMap } from '../types'; export interface IDynamicFormCallbacks { onEvent?: (eventName: TElementEvent, element: IFormEventElement) => void; @@ -15,4 +15,5 @@ export interface IDynamicFormContext { submit: () => void; callbacks: IDynamicFormCallbacks; metadata: Record; + validationParams: IDynamicFormValidationParams; } diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx index 4075f300ed..6a3718f20b 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/controls/SubmitButton/SubmitButton.unit.test.tsx @@ -53,6 +53,7 @@ describe('SubmitButton', () => { onFocus: vi.fn(), }); vi.mocked(useDynamicForm).mockReturnValue({ + validationParams: { validateOnBlur: false }, fieldHelpers: mockFieldHelpers, submit: vi.fn(), values: {}, @@ -112,6 +113,7 @@ describe('SubmitButton', () => { const mockRunTasks = vi.fn(); vi.mocked(useDynamicForm).mockReturnValue({ + validationParams: { validateOnBlur: false }, fieldHelpers: mockFieldHelpers, submit: mockSubmit, values: {}, @@ -148,6 +150,7 @@ describe('SubmitButton', () => { const mockRunTasks = vi.fn(); vi.mocked(useDynamicForm).mockReturnValue({ + validationParams: { validateOnBlur: false }, fieldHelpers: mockFieldHelpers, submit: mockSubmit, values: {}, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/defaults.ts b/packages/ui/src/components/organisms/Form/DynamicForm/defaults.ts new file mode 100644 index 0000000000..4dcec89eea --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/defaults.ts @@ -0,0 +1,5 @@ +import { IDynamicFormValidationParams } from './types'; + +export const defaultValidationParams: IDynamicFormValidationParams = { + validateOnBlur: true, +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/index.ts index 18943ce2de..679b57405f 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/index.ts @@ -1 +1,2 @@ export * from './FileField'; +export * from './hooks/useFileUpload'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.ts index f4b983bb92..67d420825e 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.ts @@ -3,18 +3,29 @@ import { IFormElement } from '../../types'; export const convertFormElementsToValidationSchema = ( elements: Array>, + schema: IValidationSchema[] = [], ): IValidationSchema[] => { - return elements.map(element => { - const validationSchema: IValidationSchema = { - id: element.id, - valueDestination: element.valueDestination, - validators: element.validate || [], - }; + const filteredElements = elements.filter( + element => element.valueDestination || element.children?.length, + ); - if (element.children) { - validationSchema.children = convertFormElementsToValidationSchema(element.children); + for (let i = 0; i < filteredElements.length; i++) { + const element = filteredElements[i]!; + + if (element.valueDestination) { + schema.push({ + id: element.id, + valueDestination: element.valueDestination, + validators: element.validate || [], + }); + + if (element.children?.length) { + schema[i]!.children = convertFormElementsToValidationSchema(element.children || []); + } + } else { + convertFormElementsToValidationSchema(element.children || [], schema); } + } - return validationSchema; - }); + return schema; }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.unit.test.ts index 29766099df..4085a73433 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/convert-form-emenents-to-validation-schema/convert-form-emenents-to-validation-schema.unit.test.ts @@ -78,7 +78,70 @@ describe('convertFormElementsToValidationSchema', () => { ], ] as const; - const cases = [case1, case2]; + const case3 = [ + [ + { + id: 'somenestedfield', + children: [ + { + id: 'field', + valueDestination: 'test', + validate: [{ type: 'required' }], + }, + { + id: 'nestedmore', + children: [ + { + id: 'nestedmore2', + valueDestination: 'test', + validate: [{ type: 'required' }], + }, + ], + }, + { + id: 'level1', + children: [ + { + id: 'level2', + children: [ + { + id: 'level3', + children: [ + { + id: 'level4', + children: [ + { + id: 'level5', + valueDestination: 'test', + validate: [{ type: 'required' }], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ] as IFormElement[], + [ + { id: 'field', valueDestination: 'test', validators: [{ type: 'required' }] }, + { + id: 'nestedmore2', + valueDestination: 'test', + validators: [{ type: 'required' }], + }, + { + id: 'level5', + valueDestination: 'test', + validators: [{ type: 'required' }], + }, + ] as const, + ] as const; + + const cases = [case1, case2, case3]; test.each(cases)('should convert form elements to validation schema', (schema, output) => { const validationSchema = convertFormElementsToValidationSchema(schema); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/get-field-definitions-from-schema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/get-field-definitions-from-schema.ts new file mode 100644 index 0000000000..2d13c8b998 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/get-field-definitions-from-schema.ts @@ -0,0 +1,26 @@ +import { IFormElement } from '../../types'; + +export const getFieldDefinitionsFromSchema = ( + elements: Array>, + definition: Array> = [], +): Array> => { + const filteredElements = elements.filter( + element => element.valueDestination || element.children?.length, + ); + + for (let i = 0; i < filteredElements.length; i++) { + const element = filteredElements[i]!; + + if (element.valueDestination) { + definition.push(element); + + if (element.children?.length) { + definition[i]!.children = getFieldDefinitionsFromSchema(element.children || []); + } + } else { + getFieldDefinitionsFromSchema(element.children || [], definition); + } + } + + return definition; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/get-field-definitions-from-schema.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/get-field-definitions-from-schema.unit.test.ts new file mode 100644 index 0000000000..5bc0c4248f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/get-field-definitions-from-schema.unit.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from 'vitest'; +import { IFormElement } from '../../types'; +import { getFieldDefinitionsFromSchema } from './get-field-definitions-from-schema'; + +describe('getFieldDefinitionsFromSchema', () => { + it('should return empty array when no elements provided', () => { + const result = getFieldDefinitionsFromSchema([]); + expect(result).toEqual([]); + }); + + it('should filter out elements without valueDestination and no children', () => { + const elements = [ + { id: '1', element: 'test' }, + { id: '2', valueDestination: 'test', element: 'test' }, + ] as Array>; + + const result = getFieldDefinitionsFromSchema(elements); + expect(result).toHaveLength(1); + expect(result[0]?.id).toBe('2'); + }); + + it('should include elements with valueDestination', () => { + const elements: Array> = [ + { id: '1', valueDestination: 'test1', element: 'test' }, + { id: '2', valueDestination: 'test2', element: 'test' }, + ] as Array>; + + const result = getFieldDefinitionsFromSchema(elements); + expect(result).toHaveLength(2); + expect(result[0]?.valueDestination).toBe('test1'); + expect(result[1]?.valueDestination).toBe('test2'); + }); + + it('should process nested children correctly', () => { + const elements: Array> = [ + { + id: '1', + valueDestination: 'parent', + element: 'test', + children: [ + { id: '1.1', valueDestination: 'child1', element: 'test' }, + { id: '1.2', valueDestination: 'child2', element: 'test' }, + ], + }, + ]; + + const result = getFieldDefinitionsFromSchema(elements); + expect(result).toHaveLength(1); + expect(result[0]?.children).toHaveLength(2); + expect(result[0]?.children?.[0]?.valueDestination).toBe('child1'); + expect(result[0]?.children?.[1]?.valueDestination).toBe('child2'); + }); + + it('should process elements with children but no valueDestination', () => { + const elements = [ + { + id: '1', + element: 'test', + children: [ + { id: '1.1', valueDestination: 'child1', element: 'test' }, + { id: '1.2', valueDestination: 'child2', element: 'test' }, + ], + }, + ] as Array>; + + const result = getFieldDefinitionsFromSchema(elements); + expect(result).toHaveLength(2); + expect(result[0]?.valueDestination).toBe('child1'); + expect(result[1]?.valueDestination).toBe('child2'); + }); + + it('should handle deeply nested structures', () => { + const elements: Array> = [ + { + id: '1', + valueDestination: 'level1', + element: 'test', + children: [ + { + id: '1.1', + valueDestination: 'level2', + element: 'test', + children: [{ id: '1.1.1', valueDestination: 'level3', element: 'test' }], + }, + ], + }, + ]; + + const result = getFieldDefinitionsFromSchema(elements); + expect(result).toHaveLength(1); + expect(result[0]?.valueDestination).toBe('level1'); + expect(result[0]?.children?.[0]?.valueDestination).toBe('level2'); + expect(result[0]?.children?.[0]?.children?.[0]?.valueDestination).toBe('level3'); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/index.ts new file mode 100644 index 0000000000..22f4c6b49c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/helpers/get-field-definitions-from-schema/index.ts @@ -0,0 +1 @@ +export * from './get-field-definitions-from-schema'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts index f008ef8062..539a4a029e 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.ts @@ -1,5 +1,5 @@ import { useRuleEngine } from '@/components/organisms/Form/hooks'; -import { TDeepthLevelStack } from '@/components/organisms/Form/Validator'; +import { TDeepthLevelStack, useValidator } from '@/components/organisms/Form/Validator'; import { useCallback, useMemo } from 'react'; import { useDynamicForm } from '../../../context'; import { IFormElement } from '../../../types'; @@ -14,8 +14,9 @@ export const useField = ( const fieldId = useElementId(element, stack); const valueDestination = useValueDestination(element, stack); - const { fieldHelpers, values } = useDynamicForm(); + const { fieldHelpers, values, validationParams } = useDynamicForm(); const { sendEvent, sendEventAsync } = useEvents(element); + const { validate } = useValidator(); const { setValue, getValue, setTouched, getTouched } = fieldHelpers; const value = useMemo(() => getValue(valueDestination), [valueDestination, getValue]); @@ -47,7 +48,12 @@ export const useField = ( const onBlur = useCallback(() => { sendEvent('onBlur'); - }, [sendEvent]); + setTouched(fieldId, true); + + if (validationParams.validateOnBlur) { + validate(); + } + }, [sendEvent, validationParams.validateOnBlur, validate, fieldId, setTouched]); const onFocus = useCallback(() => { sendEvent('onFocus'); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts index fd764c6e88..c55d8e7b5d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useField/useField.unit.test.ts @@ -1,6 +1,7 @@ import { IRuleExecutionResult, useRuleEngine } from '@/components/organisms/Form/hooks'; import { renderHook } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useValidator } from '../../../../Validator'; import { IDynamicFormContext, useDynamicForm } from '../../../context'; import { ICommonFieldParams, IFormElement } from '../../../types'; import { useEvents } from '../../internal/useEvents'; @@ -29,6 +30,10 @@ vi.mock('../../internal/useEvents', () => ({ useEvents: vi.fn(), })); +vi.mock('../../../../Validator', () => ({ + useValidator: vi.fn(), +})); + describe('useField', () => { const mockElement = { id: 'test-field', @@ -45,6 +50,7 @@ describe('useField', () => { const mockGetTouched = vi.fn(); const mockSendEvent = vi.fn(); const mockSendEventAsync = vi.fn(); + const mockValidate = vi.fn(); const mockFieldHelpers = { setValue: mockSetValue, @@ -66,7 +72,13 @@ describe('useField', () => { vi.mocked(useDynamicForm).mockReturnValue({ fieldHelpers: mockFieldHelpers, values: {}, + validationParams: { + validateOnBlur: true, + }, } as unknown as IDynamicFormContext); + vi.mocked(useValidator).mockReturnValue({ + validate: mockValidate, + } as any); mockGetValue.mockReturnValue('test-value'); mockGetTouched.mockReturnValue(false); @@ -137,12 +149,30 @@ describe('useField', () => { }); describe('onBlur', () => { - it('should trigger blur event', () => { + it('should trigger blur event and validate when validateOnBlur is true', () => { + const { result } = renderHook(() => useField(mockElement, mockStack)); + + result.current.onBlur(); + + expect(mockSendEvent).toHaveBeenCalledWith('onBlur'); + expect(mockValidate).toHaveBeenCalled(); + }); + + it('should not validate when validateOnBlur is false', () => { + vi.mocked(useDynamicForm).mockReturnValue({ + fieldHelpers: mockFieldHelpers, + values: {}, + validationParams: { + validateOnBlur: false, + }, + } as unknown as IDynamicFormContext); + const { result } = renderHook(() => useField(mockElement, mockStack)); result.current.onBlur(); expect(mockSendEvent).toHaveBeenCalledWith('onBlur'); + expect(mockValidate).not.toHaveBeenCalled(); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts index e1fbac6044..206f394062 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts @@ -5,7 +5,7 @@ export const checkIfRequired = (element: IFormElement, context: object) => { const { validate = [] } = element; const requiredLikeValidators = validate.filter( - validator => validator.type === 'required' || validator.considerRequred, + validator => validator.type === 'required' || validator.considerRequired, ); const isRequired = requiredLikeValidators.length diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts index f5eb3725ef..4594fb63f6 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts @@ -69,7 +69,7 @@ describe('checkIfRequired', () => { validate: [ { type: 'custom', - considerRequred: true, + considerRequired: true, value: {}, message: 'Field is required', }, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/index.ts deleted file mode 100644 index 50f3eded96..0000000000 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useCallbacks'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallabacks.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallabacks.unit.test.ts deleted file mode 100644 index 7de07ae2e5..0000000000 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallabacks.unit.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useDynamicForm } from '../../../context'; -import { useCallbacks } from './useCallbacks'; - -vi.mock('../../../context'); - -const mockUseDynamicForm = vi.mocked(useDynamicForm); - -describe('useCallbacks', () => { - beforeEach(() => { - vi.clearAllMocks(); - - vi.mocked(useDynamicForm).mockReturnValue({ - callbacks: { - onEvent: vi.fn(), - }, - } as any); - }); - - it('should return callbacks from context', () => { - const mockCallbacks = { - onEvent: vi.fn(), - }; - - mockUseDynamicForm.mockReturnValue({ - callbacks: mockCallbacks, - } as any); - - const result = useCallbacks(); - - expect(result).toBe(mockCallbacks); - expect(mockUseDynamicForm).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallbacks.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallbacks.ts deleted file mode 100644 index 917f2f4f27..0000000000 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useCallbacks/useCallbacks.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useDynamicForm } from '../../../context'; - -export const useCallbacks = () => { - const { callbacks } = useDynamicForm(); - - return callbacks; -}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/index.ts index dded7f0441..d88614737a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/index.ts @@ -1 +1,2 @@ +export * from './types'; export * from './useEvents'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.ts index ed87707b62..a4f199c18d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.ts @@ -3,8 +3,8 @@ import { formatValueDestination } from '@/components/organisms/Form/Validator/ut import debounce from 'lodash/debounce'; import { useCallback } from 'react'; import { useStack } from '../../../fields/FieldList/providers/StackProvider'; +import { useEventsDispatcher } from '../../../providers/EventsProvider'; import { IFormElement } from '../../../types'; -import { useCallbacks } from '../useCallbacks'; import { IFormEventElement, TElementEvent } from './types'; export interface IUseEventParams { @@ -15,7 +15,7 @@ export const useEvents = ( element: IFormElement, params: IUseEventParams = { asyncEventDelay: 500 }, ) => { - const { onEvent } = useCallbacks(); + const onEvent = useEventsDispatcher(); const { stack } = useStack(); const { asyncEventDelay } = params; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.unit.test.ts index 99791cc0c1..91faa45fbc 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents.unit.test.ts @@ -3,19 +3,19 @@ import { formatValueDestination } from '@/components/organisms/Form/Validator/ut import { renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useStack } from '../../../fields/FieldList/providers/StackProvider'; -import { useCallbacks } from '../useCallbacks'; +import { useEventsDispatcher } from '../../../providers/EventsProvider'; import { IFormEventElement } from './types'; import { useEvents } from './useEvents'; vi.mock('@/components/organisms/Form/Validator/utils/format-id'); vi.mock('@/components/organisms/Form/Validator/utils/format-value-destination'); vi.mock('../../../fields/FieldList/providers/StackProvider'); -vi.mock('../useCallbacks'); +vi.mock('../../../providers/EventsProvider'); const mockFormatId = vi.mocked(formatId); const mockFormatValueDestination = vi.mocked(formatValueDestination); const mockUseStack = vi.mocked(useStack); -const mockUseCallbacks = vi.mocked(useCallbacks); +const mockUseEventsDispatcher = vi.mocked(useEventsDispatcher); describe('useEvents', () => { const mockElement = { @@ -29,12 +29,10 @@ describe('useEvents', () => { beforeEach(() => { vi.clearAllMocks(); - mockUseCallbacks.mockReturnValue({ onEvent: mockOnEvent }); + mockUseEventsDispatcher.mockReturnValue(mockOnEvent); mockUseStack.mockReturnValue({ stack: mockStack }); mockFormatId.mockReturnValue('formatted-id'); mockFormatValueDestination.mockReturnValue('formatted.destination'); - mockUseCallbacks.mockReturnValue({ onEvent: mockOnEvent }); - vi.mocked(useCallbacks).mockReturnValue({ onEvent: mockOnEvent }); }); it('should return sendEvent and sendEventAsync functions', () => { @@ -67,13 +65,6 @@ describe('useEvents', () => { expect(mockFormatValueDestination).toHaveBeenCalledWith('test.destination', []); }); - it('should handle undefined onEvent callback', () => { - mockUseCallbacks.mockReturnValue({ onEvent: undefined }); - const { result } = renderHook(() => useEvents(mockElement)); - - expect(() => result.current.sendEvent('onFocus')).not.toThrow(); - }); - it('should use default asyncEventDelay when not provided', () => { const { result } = renderHook(() => useEvents(mockElement)); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts index 71401e9d6a..ae87d4830c 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.ts @@ -1,9 +1,12 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; +import { getFieldDefinitionsFromSchema } from '../../../helpers/get-field-definitions-from-schema'; import { IFormElement } from '../../../types'; import { generateTouchedMapForAllElements } from './helpers/generate-touched-map-for-all-elements/generate-touched-map-for-all-elements'; import { ITouchedState } from './types'; -export const useTouched = (elements: Array>, context: object) => { +export const useTouched = (_elements: Array>, context: object) => { + const elements = useMemo(() => getFieldDefinitionsFromSchema(_elements), [_elements]); + const [touched, setTouchedState] = useState({}); const setFieldTouched = useCallback((fieldName: string, isTouched: boolean) => { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts index 9d8096fb01..4fd2998b53 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useTouched/useTouched.unit.test.ts @@ -11,6 +11,10 @@ vi.mock( }), ); +vi.mock('../../../helpers/get-field-definitions-from-schema', () => ({ + getFieldDefinitionsFromSchema: vi.fn(elements => elements), +})); + describe('useTouched', () => { const elements: IFormElement[] = [ { id: '1', valueDestination: '1', children: [], validate: [], element: 'textinput' }, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts index 07ddd06181..9cc643600c 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/internal/useValidationSchema/useValidationSchema.ts @@ -4,9 +4,10 @@ import { convertFormElementsToValidationSchema } from '../../../helpers/convert- import { IFormElement } from '../../../types'; export const useValidationSchema = (elements: Array>) => { - const validationSchema = useMemo(() => { - return convertFormElementsToValidationSchema(elements); - }, [elements]); + const validationSchema = useMemo( + () => convertFormElementsToValidationSchema(elements), + [elements], + ); return validationSchema; }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/index.ts index eb74bf18ce..afbbf0e403 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/index.ts @@ -2,4 +2,5 @@ export * from './context/hooks/useDynamicForm'; export * from './DynamicForm'; export * from './fields'; export * from './hooks/external'; +export * from './providers/EventsProvider'; export * from './types'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/EventsProvider.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/EventsProvider.tsx new file mode 100644 index 0000000000..ad66d4308a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/EventsProvider.tsx @@ -0,0 +1,15 @@ +import { FunctionComponent } from 'react'; +import { IFormEventElement, TElementEvent } from '../../hooks/internal/useEvents/types'; +import { EventsProvierContext } from './context'; +import { useEventsPool } from './hooks/internal/useEventsPool'; + +export interface IEventsProviderProps { + children: React.ReactNode; + onEvent?: (eventName: TElementEvent, element: IFormEventElement) => void; +} + +export const EventsProvider: FunctionComponent = ({ children, onEvent }) => { + const context = useEventsPool(onEvent); + + return {children}; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/EventsProvider.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/EventsProvider.unit.test.tsx new file mode 100644 index 0000000000..b2ddf85e1a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/EventsProvider.unit.test.tsx @@ -0,0 +1,64 @@ +import { render } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { IFormEventElement, TElementEvent } from '../../hooks/internal/useEvents/types'; +import { EventsProvider } from './EventsProvider'; +import { useEventsPool } from './hooks/internal/useEventsPool'; + +vi.mock('./hooks/internal/useEventsPool', () => ({ + useEventsPool: vi.fn(), +})); + +describe('EventsProvider', () => { + const mockContext = { + subscribe: vi.fn(), + unsubscribe: vi.fn(), + run: vi.fn(), + event: vi.fn(), + listeners: [], + }; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useEventsPool).mockReturnValue(mockContext); + }); + + it('should render children', () => { + const { getByText } = render( + +
Test Child
+
, + ); + + expect(getByText('Test Child')).toBeInTheDocument(); + }); + + it('should call useEventsPool with onEvent prop', () => { + const mockOnEvent = vi.fn(); + render(Child); + + expect(useEventsPool).toHaveBeenCalledWith(mockOnEvent); + }); + + it('should provide context value from useEventsPool', () => { + const mockOnEvent = (eventName: TElementEvent, element: IFormEventElement) => { + console.log(eventName, element); + }; + render( + +
Child
+
, + ); + + expect(useEventsPool).toHaveBeenCalledWith(mockOnEvent); + }); + + it('should work without onEvent prop', () => { + render( + +
Child
+
, + ); + + expect(useEventsPool).toHaveBeenCalledWith(undefined); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/context/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/context/index.ts new file mode 100644 index 0000000000..470a7dc9d2 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/context/index.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import { IEventsProviderContext } from '../types'; + +export const EventsProvierContext = createContext({} as IEventsProviderContext); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/index.ts new file mode 100644 index 0000000000..6ed716e47c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/index.ts @@ -0,0 +1 @@ +export * from './useEventsConsumer'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/useEventsConsumer.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/useEventsConsumer.ts new file mode 100644 index 0000000000..4087cedd73 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/useEventsConsumer.ts @@ -0,0 +1,23 @@ +import { useEffect, useRef } from 'react'; +import { IEventsListener } from '../../../types'; +import { useEventsProvider } from '../../internal/useEventsProvider'; + +export const useEventsConsumer = (listener: IEventsListener) => { + const { subscribe, unsubscribe } = useEventsProvider(); + + const subscribeRef = useRef(subscribe); + const unsubscribeRef = useRef(unsubscribe); + + useEffect(() => { + subscribeRef.current = subscribe; + unsubscribeRef.current = unsubscribe; + }, [subscribe, unsubscribe]); + + useEffect(() => { + subscribeRef.current(listener); + + return () => { + unsubscribeRef.current(listener); + }; + }, [subscribeRef, unsubscribeRef, listener]); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/useEventsConsumer.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/useEventsConsumer.unit.test.ts new file mode 100644 index 0000000000..e506976f21 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsConsumer/useEventsConsumer.unit.test.ts @@ -0,0 +1,63 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { IEventsListener, IEventsProviderContext } from '../../../types'; +import { useEventsProvider } from '../../internal/useEventsProvider'; +import { useEventsConsumer } from './useEventsConsumer'; + +vi.mock('../../internal/useEventsProvider', () => ({ + useEventsProvider: vi.fn(), +})); + +describe('useEventsConsumer', () => { + const mockSubscribe = vi.fn(); + const mockUnsubscribe = vi.fn(); + const mockListener = { + id: '1', + eventName: 'onChange', + callback: vi.fn(), + } as unknown as IEventsListener; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useEventsProvider).mockReturnValue({ + subscribe: mockSubscribe, + unsubscribe: mockUnsubscribe, + } as unknown as IEventsProviderContext); + }); + + it('should call useEventsProvider', () => { + renderHook(() => useEventsConsumer(mockListener)); + expect(useEventsProvider).toHaveBeenCalled(); + }); + + it('should subscribe listener on mount', () => { + renderHook(() => useEventsConsumer(mockListener)); + expect(mockSubscribe).toHaveBeenCalledWith(mockListener); + }); + + it('should unsubscribe listener on unmount', () => { + const { unmount } = renderHook(() => useEventsConsumer(mockListener)); + unmount(); + expect(mockUnsubscribe).toHaveBeenCalledWith(mockListener); + }); + + it('should update refs when subscribe/unsubscribe change', () => { + const newMockSubscribe = vi.fn(); + const newMockUnsubscribe = vi.fn(); + + const { rerender, unmount } = renderHook(() => useEventsConsumer(mockListener)); + + vi.mocked(useEventsProvider).mockReturnValue({ + subscribe: newMockSubscribe, + unsubscribe: newMockUnsubscribe, + } as unknown as IEventsProviderContext); + + rerender(); + + // Unmount to test if new unsubscribe is called + unmount(); + + expect(newMockUnsubscribe).toHaveBeenCalledWith(mockListener); + expect(mockUnsubscribe).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/index.ts new file mode 100644 index 0000000000..0084e21567 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/index.ts @@ -0,0 +1 @@ +export * from './useEventsDispatcher'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/useEventsDispatcher.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/useEventsDispatcher.ts new file mode 100644 index 0000000000..e78867d18b --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/useEventsDispatcher.ts @@ -0,0 +1,7 @@ +import { useEventsProvider } from '../../internal/useEventsProvider'; + +export const useEventsDispatcher = () => { + const { event } = useEventsProvider(); + + return event; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/useEventsDispatcher.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/useEventsDispatcher.unit.test.ts new file mode 100644 index 0000000000..bde0bb5d5a --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/external/useEventsDispatcher/useEventsDispatcher.unit.test.ts @@ -0,0 +1,31 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { IEventsProviderContext } from '../../../types'; +import { useEventsProvider } from '../../internal/useEventsProvider'; +import { useEventsDispatcher } from './useEventsDispatcher'; + +vi.mock('../../internal/useEventsProvider', () => ({ + useEventsProvider: vi.fn(), +})); + +describe('useEventsDispatcher', () => { + const mockEvent = vi.fn(); + + beforeEach(() => { + vi.mocked(useEventsProvider).mockReturnValue({ + event: mockEvent, + } as unknown as IEventsProviderContext); + }); + + it('should return event from useEventsProvider', () => { + const { result } = renderHook(() => useEventsDispatcher()); + + expect(result.current).toBe(mockEvent); + }); + + it('should call useEventsProvider', () => { + renderHook(() => useEventsDispatcher()); + + expect(useEventsProvider).toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/index.ts new file mode 100644 index 0000000000..c70be07274 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/index.ts @@ -0,0 +1 @@ +export * from './useEventsPool'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/useEventsPool.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/useEventsPool.ts new file mode 100644 index 0000000000..54e68eb1fd --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/useEventsPool.ts @@ -0,0 +1,62 @@ +import { + IFormEventElement, + TElementEvent, +} from '@/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types'; +import { useCallback, useState } from 'react'; +import { IEventsProviderProps } from '../../../EventsProvider'; +import { IEventsListener } from '../../../types'; + +export const useEventsPool = (onEvent: IEventsProviderProps['onEvent']) => { + const [listeners, setListeners] = useState([]); + + const subscribe = useCallback((listener: IEventsListener) => { + setListeners(prev => { + const isListenerExists = prev.find( + l => l.id === listener.id && l.eventName === listener.eventName, + ); + + if (isListenerExists) { + return prev.map(prevListener => + prevListener.id === listener.id && prevListener.eventName === listener.eventName + ? listener + : prevListener, + ); + } + + return [...prev, listener]; + }); + }, []); + + const unsubscribe = useCallback((listener: IEventsListener) => { + setListeners(prev => + prev.filter(l => l.id !== listener.id && l.eventName !== listener.eventName), + ); + }, []); + + const run = useCallback( + (eventName: TElementEvent, element: IFormEventElement) => { + listeners.forEach(listener => { + if (listener.eventName === eventName && listener.id === element.id) { + listener.callback(eventName, element); + } + }); + }, + [listeners], + ); + + const event = useCallback( + (eventName: TElementEvent, element: IFormEventElement) => { + run(eventName, element); + onEvent?.(eventName, element); + }, + [run, onEvent], + ); + + return { + listeners, + subscribe, + unsubscribe, + run, + event, + }; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/useEventsPool.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/useEventsPool.unit.test.tsx new file mode 100644 index 0000000000..993f4b3515 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsPool/useEventsPool.unit.test.tsx @@ -0,0 +1,125 @@ +import { IFormEventElement } from '@/components/organisms/Form/DynamicForm/hooks/internal/useEvents/types'; +import { act, renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { IEventsListener } from '../../../types'; +import { useEventsPool } from './useEventsPool'; + +describe('useEventsPool', () => { + const mockOnEvent = vi.fn(); + const mockElement = { + id: 'test-id', + valueDestination: 'test', + formattedId: 'test-id', + formattedValueDestination: 'test', + element: 'test', + } as IFormEventElement; + + it('should initialize with empty listeners array', () => { + const { result } = renderHook(() => useEventsPool(mockOnEvent)); + expect(result.current.listeners).toEqual([]); + }); + + it('should subscribe a new listener', () => { + const { result } = renderHook(() => useEventsPool(mockOnEvent)); + const listener = { + id: 'test-id', + eventName: 'onChange', + callback: vi.fn(), + } as IEventsListener; + + act(() => { + result.current.subscribe(listener); + }); + + expect(result.current.listeners).toHaveLength(1); + expect(result.current.listeners[0]).toEqual(listener); + }); + + it('should not subscribe duplicate listener', () => { + const { result } = renderHook(() => useEventsPool(mockOnEvent)); + const listener = { + id: 'test-id', + eventName: 'onChange', + callback: vi.fn(), + } as IEventsListener; + + act(() => { + result.current.subscribe(listener); + result.current.subscribe(listener); + }); + + expect(result.current.listeners).toHaveLength(1); + }); + + it('should unsubscribe a listener', () => { + const { result } = renderHook(() => useEventsPool(mockOnEvent)); + const listener = { + id: 'test-id', + eventName: 'onChange', + callback: vi.fn(), + } as IEventsListener; + + act(() => { + result.current.subscribe(listener); + result.current.unsubscribe(listener); + }); + + expect(result.current.listeners).toHaveLength(0); + }); + + it('should run event for matching listeners', () => { + const { result, rerender } = renderHook(() => useEventsPool(mockOnEvent)); + + const mockElement = { + id: 'test-id-1', + valueDestination: 'test', + formattedId: 'test-id-1', + formattedValueDestination: 'test', + element: 'test', + } as IFormEventElement; + + const listener1 = { + id: 'test-id-1', + eventName: 'onChange', + callback: vi.fn(), + } as IEventsListener; + + const listener2 = { + id: 'test-id-2', + eventName: 'onBlur', + callback: vi.fn(), + } as IEventsListener; + + act(() => { + result.current.subscribe(listener1); + result.current.subscribe(listener2); + }); + + rerender(); + + act(() => { + result.current.run('onChange', mockElement); + }); + + expect(listener1.callback).toHaveBeenCalledWith('onChange', mockElement); + expect(listener2.callback).not.toHaveBeenCalled(); + }); + + it('should trigger event and call onEvent callback', () => { + const { result, rerender } = renderHook(() => useEventsPool(mockOnEvent)); + const listener = { + id: 'test-id', + eventName: 'onChange', + callback: vi.fn(), + } as IEventsListener; + + result.current.subscribe(listener); + + rerender(); + + result.current.event('onChange', mockElement); + + expect(listener.callback).toHaveBeenCalledWith('onChange', mockElement); + expect(mockOnEvent).toHaveBeenCalledWith('onChange', mockElement); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/index.ts new file mode 100644 index 0000000000..44de6f202e --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/index.ts @@ -0,0 +1 @@ +export * from './useEventsProvider'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/useEventsProvider.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/useEventsProvider.ts new file mode 100644 index 0000000000..affe32af23 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/useEventsProvider.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { EventsProvierContext } from '../../../context'; + +export const useEventsProvider = () => useContext(EventsProvierContext); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/useEventsProvider.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/useEventsProvider.unit.test.ts new file mode 100644 index 0000000000..2a8cef0b64 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/hooks/internal/useEventsProvider/useEventsProvider.unit.test.ts @@ -0,0 +1,32 @@ +import { renderHook } from '@testing-library/react'; +import { useContext } from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { EventsProvierContext } from '../../../context'; +import { useEventsProvider } from './useEventsProvider'; + +vi.mock('react', () => ({ + createContext: vi.fn(), + useContext: vi.fn(), +})); + +describe('useEventsProvider', () => { + it('should call useContext with EventsProvierContext', () => { + renderHook(() => useEventsProvider()); + expect(useContext).toHaveBeenCalledWith(EventsProvierContext); + }); + + it('should return context value', () => { + const mockContextValue = { + listeners: [], + subscribe: vi.fn(), + unsubscribe: vi.fn(), + run: vi.fn(), + event: vi.fn(), + }; + + vi.mocked(useContext).mockReturnValue(mockContextValue); + + const { result } = renderHook(() => useEventsProvider()); + expect(result.current).toBe(mockContextValue); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/index.ts new file mode 100644 index 0000000000..c890e12251 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/index.ts @@ -0,0 +1,4 @@ +export * from './EventsProvider'; +export * from './hooks/external/useEventsConsumer'; +export * from './hooks/external/useEventsDispatcher'; +export * from './types'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/types.ts b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/types.ts new file mode 100644 index 0000000000..a3ccbdc88f --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/providers/EventsProvider/types.ts @@ -0,0 +1,15 @@ +import { IFormEventElement, TElementEvent } from '../../hooks/internal/useEvents/types'; + +export interface IEventsListener { + id: string; + eventName: TElementEvent; + callback: (eventName: TElementEvent, element: IFormEventElement) => void; +} + +export interface IEventsProviderContext { + subscribe: (listener: IEventsListener) => void; + unsubscribe: (listener: IEventsListener) => void; + run: (eventName: TElementEvent, element: IFormEventElement) => void; + event: (eventName: TElementEvent, element: IFormEventElement) => void; + listeners: IEventsListener[]; +} diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts index 9911d4401b..3ba4374519 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts @@ -1,7 +1,7 @@ import { FunctionComponent } from 'react'; import { IRule } from '../../hooks/useRuleEngine'; import { IValidationError, IValidationParams, TValidators } from '../../Validator'; -import { IFormEventElement, TElementEvent } from '../hooks/internal/useEvents/types'; +import { IEventsProviderProps } from '../providers/EventsProvider'; export interface ICommonFieldParams { label?: string; @@ -34,6 +34,7 @@ export type TDynamicFormElement< TParams = object, > = FunctionComponent<{ element: IFormElement; + children?: React.ReactNode | React.ReactNode[]; }>; export type TDynamicFormField = FunctionComponent<{ @@ -43,18 +44,25 @@ export type TDynamicFormField = FunctionComponent<{ export type TElementsMap = Record>; +export interface IDynamicFormValidationParams extends IValidationParams { + validateOnBlur?: boolean; +} + export interface IDynamicFormProps { values: TValues; elements: Array>; - fieldExtends?: Record>; + fieldExtends?: Record | TDynamicFormElement>; - validationParams?: IValidationParams; + validationParams?: IDynamicFormValidationParams; onChange?: (newValues: TValues) => void; onFieldChange?: (fieldName: string, newValue: unknown, newValues: TValues) => void; onSubmit?: (values: TValues) => void; - onEvent?: (eventName: TElementEvent, element: IFormEventElement) => void; + onEvent?: IEventsProviderProps['onEvent']; ref?: React.RefObject>; metadata?: Record; } + +export type { IFormEventElement, TElementEvent } from '../hooks/internal/useEvents'; +export type { TBaseFields } from '../repositories'; diff --git a/packages/ui/src/components/organisms/Form/Validator/types/index.ts b/packages/ui/src/components/organisms/Form/Validator/types/index.ts index 8223a57e27..75112cac37 100644 --- a/packages/ui/src/components/organisms/Form/Validator/types/index.ts +++ b/packages/ui/src/components/organisms/Form/Validator/types/index.ts @@ -18,7 +18,7 @@ export interface ICommonValidator = ( - value: T, - validator: ICommonValidator, -) => void; +export type TValidator< + T, + TValidatorParams = unknown, + TValidatorType extends string = TBaseValidators, +> = (value: T, validator: ICommonValidator) => void; export type TDeepthLevelStack = number[]; diff --git a/packages/ui/src/components/organisms/index.ts b/packages/ui/src/components/organisms/index.ts index 6efad98e13..df5d446a5e 100644 --- a/packages/ui/src/components/organisms/index.ts +++ b/packages/ui/src/components/organisms/index.ts @@ -1,4 +1,5 @@ export * from './DataTable'; export * from './DynamicForm'; export * from './Form'; +export * from './Renderer'; export * from './WorkflowsTable'; diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations index 4c63edc33f..55381b3fd0 160000 --- a/services/workflows-service/prisma/data-migrations +++ b/services/workflows-service/prisma/data-migrations @@ -1 +1 @@ -Subproject commit 4c63edc33f9b66121944c241f1f1f2c4979a7ea5 +Subproject commit 55381b3fd001b9d1e75489ee6dd46c41b4050d3f From 1fdbda4b554524c5087a50a413a6ac473f294a14 Mon Sep 17 00:00:00 2001 From: Sasha Date: Sun, 5 Jan 2025 21:52:35 +0100 Subject: [PATCH 35/54] fix: better violation names on statistics page (BAL-3294) (#2932) * feat: better violation names on statistics page * fix: CodeRabbit comments --------- Co-authored-by: Alon Peretz <8467965+alonp99@users.noreply.github.com> --- .../useBusinessReportMetricsQuery.ts | 8 ++++- .../PortfolioRiskStatistics.tsx | 4 +-- .../usePortfolioRiskStatisticsLogic.tsx | 5 ++-- .../dtos/business-report-metrics-dto.ts | 30 ++++++++++++++----- .../merchant-monitoring-client.ts | 8 ++++- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportMetricsQuery/useBusinessReportMetricsQuery.ts b/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportMetricsQuery/useBusinessReportMetricsQuery.ts index f22904ae54..c9c6de69e6 100644 --- a/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportMetricsQuery/useBusinessReportMetricsQuery.ts +++ b/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportMetricsQuery/useBusinessReportMetricsQuery.ts @@ -12,7 +12,13 @@ export const MetricsResponseSchema = z.object({ high: z.number(), critical: z.number(), }), - violationCounts: z.record(z.string(), z.number()), + violationCounts: z.array( + z.object({ + name: z.string(), + id: z.string(), + count: z.number(), + }), + ), }); export const fetchBusinessReportMetrics = async () => { diff --git a/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/PortfolioRiskStatistics.tsx b/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/PortfolioRiskStatistics.tsx index 5185f5ec40..8248120f68 100644 --- a/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/PortfolioRiskStatistics.tsx +++ b/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/PortfolioRiskStatistics.tsx @@ -172,11 +172,11 @@ export const PortfolioRiskStatistics: FunctionComponent - {filteredRiskIndicators.map(({ name, count }, index) => ( + {filteredRiskIndicators.map(({ name, count, id }, index) => ( diff --git a/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/hooks/usePortfolioRiskStatisticsLogic/usePortfolioRiskStatisticsLogic.tsx b/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/hooks/usePortfolioRiskStatisticsLogic/usePortfolioRiskStatisticsLogic.tsx index f9effccab4..317377c19d 100644 --- a/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/hooks/usePortfolioRiskStatisticsLogic/usePortfolioRiskStatisticsLogic.tsx +++ b/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/hooks/usePortfolioRiskStatisticsLogic/usePortfolioRiskStatisticsLogic.tsx @@ -20,11 +20,10 @@ export const usePortfolioRiskStatisticsLogic = ({ }, [], ); - const totalRiskIndicators = Object.values(violationCounts).reduce((acc, curr) => acc + curr, 0); + const totalRiskIndicators = violationCounts.reduce((acc, { count }) => acc + count, 0); const filteredRiskIndicators = useMemo( () => - Object.entries(violationCounts) - .map(([name, count]) => ({ name, count })) + violationCounts .sort((a, b) => (riskIndicatorsSorting === 'asc' ? a.count - b.count : b.count - a.count)) .slice(0, 5), [violationCounts, riskIndicatorsSorting], diff --git a/services/workflows-service/src/business-report/dtos/business-report-metrics-dto.ts b/services/workflows-service/src/business-report/dtos/business-report-metrics-dto.ts index ab75feabd6..9b3798de43 100644 --- a/services/workflows-service/src/business-report/dtos/business-report-metrics-dto.ts +++ b/services/workflows-service/src/business-report/dtos/business-report-metrics-dto.ts @@ -1,5 +1,5 @@ -import { IsNumber, IsObject, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; +import { IsArray, IsNumber, IsString, ValidateNested } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; @@ -47,12 +47,26 @@ export class BusinessReportMetricsDto { riskLevelCounts!: RiskLevelCountsDto; @ApiProperty({ - description: 'Counts of violations by type', - example: { PROHIBITED_CONTENT: 2, MISSING_INFORMATION: 1 }, - type: 'object', - additionalProperties: { type: 'number' }, + description: 'Detected violations counts', + example: [{ id: 'PROHIBITED_CONTENT', name: 'Prohibited content', count: 2 }], + type: 'array', }) - @IsObject() - @Type(() => Object) - violationCounts!: Record; + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ViolationCountDto) + violationCounts!: ViolationCountDto[]; +} + +export class ViolationCountDto { + @ApiProperty() + @IsString() + id!: string; + + @ApiProperty() + @IsString() + name!: string; + + @ApiProperty() + @IsNumber() + count!: number; } diff --git a/services/workflows-service/src/business-report/merchant-monitoring-client.ts b/services/workflows-service/src/business-report/merchant-monitoring-client.ts index fc7afa33e6..cc72de82b0 100644 --- a/services/workflows-service/src/business-report/merchant-monitoring-client.ts +++ b/services/workflows-service/src/business-report/merchant-monitoring-client.ts @@ -64,7 +64,13 @@ const MetricsResponseSchema = z.object({ high: z.number(), critical: z.number(), }), - violationCounts: z.record(z.string(), z.number()), + violationCounts: z.array( + z.object({ + name: z.string(), + id: z.string(), + count: z.number(), + }), + ), }); @Injectable() From d48566057a7bc57e16d2372a7e6f8343481517e7 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Tue, 7 Jan 2025 11:01:50 +0200 Subject: [PATCH 36/54] fix: renamed property in applyWhen rule --- .../_stories/ConditionalRenderingShowcase/schema.ts | 4 ++-- .../Form/DynamicForm/_stories/ValidationShowcase/schema.ts | 2 +- .../helpers/check-if-required/check-if-required.ts | 7 +------ .../check-if-required/check-if-required.unit.test.ts | 4 ++-- .../Form/Validator/_stories/components/Story/schema.ts | 2 +- .../src/components/organisms/Form/Validator/types/index.ts | 2 +- .../Form/Validator/utils/validate/validate.unit.test.ts | 4 ++-- 7 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts index 44aee0761d..6c063f8ec9 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts @@ -15,7 +15,7 @@ export const schema: Array> = [ value: {}, message: 'First name is required', applyWhen: { - type: 'json-logic', + engine: 'json-logic', value: { '!': { var: 'forceEverythingOptionnal' }, }, @@ -60,7 +60,7 @@ export const schema: Array> = [ value: {}, message: 'Last name is required', applyWhen: { - type: 'json-logic', + engine: 'json-logic', value: { and: [{ '!!': { var: 'firstName' } }, { '!': { var: 'forceEverythingOptionnal' } }], }, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts index 50c12c7b9c..4fcf7de0ae 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts @@ -63,7 +63,7 @@ export const schema: Array> = [ value: {}, message: 'Passport photo is required', applyWhen: { - type: 'json-logic', + engine: 'json-logic', value: { '!': { var: 'iDontHaveDocument' }, }, diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts index 206f394062..7e2ef428f5 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts @@ -12,12 +12,7 @@ export const checkIfRequired = (element: IFormElement, context: object) => { ? requiredLikeValidators.some(validator => { const { applyWhen } = validator; const shouldValidate = applyWhen - ? executeRules(context, [ - { - engine: applyWhen.type, - value: applyWhen.value, - }, - ]).every(result => result.result) + ? executeRules(context, [applyWhen]).every(result => result.result) : true; if (!shouldValidate) return false; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts index 4594fb63f6..a501404abb 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts @@ -92,7 +92,7 @@ describe('checkIfRequired', () => { value: {}, message: 'Field is required', applyWhen: { - type: 'json-logic', + engine: 'json-logic', value: { '==': [{ var: 'someField' }, true] }, }, }, @@ -125,7 +125,7 @@ describe('checkIfRequired', () => { value: {}, message: 'Field is required', applyWhen: { - type: 'json-logic', + engine: 'json-logic', value: { '==': [{ var: 'someField' }, true] }, }, }, diff --git a/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/schema.ts b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/schema.ts index b73791c2b5..37993bbf96 100644 --- a/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/schema.ts +++ b/packages/ui/src/components/organisms/Form/Validator/_stories/components/Story/schema.ts @@ -42,7 +42,7 @@ export const initialSchema: IValidationSchema[] = [ message: 'Age is required', value: {}, applyWhen: { - type: 'json-logic', + engine: 'json-logic', value: { and: [{ '!!': { var: 'firstName' } }, { '!!': { var: 'lastName' } }], }, diff --git a/packages/ui/src/components/organisms/Form/Validator/types/index.ts b/packages/ui/src/components/organisms/Form/Validator/types/index.ts index 75112cac37..03956ea59d 100644 --- a/packages/ui/src/components/organisms/Form/Validator/types/index.ts +++ b/packages/ui/src/components/organisms/Form/Validator/types/index.ts @@ -1,7 +1,7 @@ export type TBaseValidationRules = 'json-logic'; export interface IValidationRule { - type: TBaseValidationRules; + engine: TBaseValidationRules; value: object; } diff --git a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts index d338ef4fa1..2e13e79e0e 100644 --- a/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/utils/validate/validate.unit.test.ts @@ -749,7 +749,7 @@ describe('validate', () => { message: 'Field is required.', value: {}, applyWhen: { - type: 'json-logic', + engine: 'json-logic', value: { var: 'firstName', }, @@ -781,7 +781,7 @@ describe('validate', () => { message: 'Field is required.', value: {}, applyWhen: { - type: 'json-logic', + engine: 'json-logic', value: { '==': [{ var: 'firstName' }, 'Banana'], }, From a30f028918c0067eb640e8845a176098597f2eae Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Tue, 7 Jan 2025 11:17:29 +0200 Subject: [PATCH 37/54] feat: added url format & refactor --- .../FileField/hooks/useFileUpload/helpers.ts | 15 +------ .../hooks/useFileUpload/useFileUpload.ts | 2 + .../Form/DynamicForm/utils/format-string.ts | 6 +++ .../utils/format-string.unit.test.ts | 43 +++++++++++++++++++ 4 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/utils/format-string.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/utils/format-string.unit.test.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.ts index 48c89841e9..e25eba85ec 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/helpers.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import get from 'lodash/get'; +import { formatString } from '../../../../utils/format-string'; import { IFileFieldParams } from '../../FileField'; export const formatHeaders = ( @@ -9,19 +10,7 @@ export const formatHeaders = ( const formattedHeaders: Record = {}; Object.entries(headers).forEach(([key, value]) => { - let formattedValue = value; - const matches = value.match(/\{([^}]+)\}/g); - - if (matches) { - matches.forEach(match => { - const metadataKey = match.slice(1, -1); - - if (metadata[metadataKey]) { - formattedValue = formattedValue.replace(match, metadata[metadataKey]); - } - }); - } - + const formattedValue = formatString(value, metadata); formattedHeaders[key] = formattedValue; }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts index a62703f7a8..fc32a66507 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts @@ -4,6 +4,7 @@ import { useElement, useField } from '../../../../hooks/external'; import { useTaskRunner } from '../../../../providers/TaskRunner/hooks/useTaskRunner'; import { ITask } from '../../../../providers/TaskRunner/types'; import { IFormElement } from '../../../../types'; +import { formatString } from '../../../../utils/format-string'; import { useStack } from '../../../FieldList/providers/StackProvider'; import { IFileFieldParams } from '../../FileField'; import { formatHeaders, uploadFile } from './helpers'; @@ -38,6 +39,7 @@ export const useFileUpload = ( ...uploadSettings, method: uploadSettings?.method || 'POST', headers: formatHeaders(uploadSettings?.headers || {}, metadata), + url: formatString(uploadSettings?.url || '', metadata), }; if (uploadOn === 'change') { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-string.ts b/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-string.ts new file mode 100644 index 0000000000..75ad28ce66 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-string.ts @@ -0,0 +1,6 @@ +export const formatString = (string: string, metadata: Record = {}) => { + // Replace patterns like {key} with corresponding metadata values + return string.replace(/\{([^}]+)\}/g, (match, key) => { + return metadata[key] || match; + }); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-string.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-string.unit.test.ts new file mode 100644 index 0000000000..e47d058726 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/utils/format-string.unit.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import { formatString } from './format-string'; + +describe('formatString', () => { + it('should return original string if no matches found', () => { + const input = 'test string'; + const metadata = { key: 'value' }; + + const result = formatString(input, metadata); + + expect(result).toBe(input); + }); + + it('should replace single placeholder with metadata value', () => { + const input = 'Hello {name}'; + const metadata = { name: 'John' }; + + const result = formatString(input, metadata); + + expect(result).toBe('Hello John'); + }); + + it('should replace multiple placeholders with metadata values', () => { + const input = '{greeting} {name}'; + const metadata = { + greeting: 'Hello', + name: 'John', + }; + + const result = formatString(input, metadata); + + expect(result).toBe('Hello John'); + }); + + it('should keep placeholders unchanged when metadata is empty', () => { + const input = 'Hello {name}, your ID is {userId}'; + const metadata = {}; + + const result = formatString(input, metadata); + + expect(result).toBe('Hello {name}, your ID is {userId}'); + }); +}); From 8019fd8b55b79ad466858b761184cde017844cdb Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Tue, 7 Jan 2025 15:30:48 +0200 Subject: [PATCH 38/54] Bal 3242 (#2939) * feat: fixed types & exports & verbose logging & submit event * feat: implemented plugins runner * feat: updated types & minor adjustments to format string * fix: document validator fixes & updated metadata * feat: added submit button lock while tasks running * feat: added metadata to useField & useElement & added missing format validator * feat: added ref to form & fixed types & tests * fix: fixed plugins context update & added plugin listeners --- .../domains/collection-flow/types/index.ts | 2 + .../pages/CollectionFlow/CollectionFlow.tsx | 11 +- .../CollectionFlowUI/CollectionFlowUI.tsx | 62 ++++++- .../utility/PluginsRunner/PluginsRunner.tsx | 15 ++ .../PluginsRunner/PluginsRunner.unit.test.tsx | 67 ++++++++ .../utility/PluginsRunner/context/context.ts | 4 + .../utility/PluginsRunner/context/index.ts | 2 + .../utility/PluginsRunner/context/types.ts | 22 +++ .../hooks/external/usePlugins/index.ts | 1 + .../hooks/external/usePlugins/usePlugins.ts | 4 + .../usePlugins/usePlugins.unit.test.ts | 30 ++++ .../external/usePluginsSubscribe/index.ts | 1 + .../usePluginsSubscribe.ts | 15 ++ .../usePluginsSubscribe.unit.test.ts | 49 ++++++ .../hooks/internal/usePluginsRunner/index.ts | 1 + .../usePluginsRunner/usePluginListeners.ts | 36 ++++ .../usePluginListeners.unit.test.ts | 89 ++++++++++ .../usePluginsRunner/usePluginsRunner.ts | 104 ++++++++++++ .../usePluginsRunner.unit.test.ts | 136 +++++++++++++++ .../components/utility/PluginsRunner/index.ts | 3 + .../PluginsRunner/plugins.repository.ts | 17 ++ .../plugins.repository.unit.test.ts | 21 +++ .../PluginsRunner/plugins/event.plugin.ts | 17 ++ .../plugins/event.plugin.unit.test.ts | 53 ++++++ .../PluginsRunner/plugins/ocr.plugin.ts | 13 ++ .../components/utility/PluginsRunner/types.ts | 28 ++++ .../hooks/useAppMetadata/index.ts | 1 + .../hooks/useAppMetadata/types.ts | 4 + .../hooks/useAppMetadata/useAppMetadata.ts | 12 ++ .../hooks/usePluginsHandler/helpers.ts | 14 ++ .../hooks/usePluginsHandler/index.ts | 0 .../usePluginsHandler/usePluginRunners.ts | 37 +++++ .../usePluginRunners.unit.test.ts | 100 +++++++++++ .../usePluginsHandler/usePluginsHandler.ts | 39 +++++ .../usePluginsHandler.unit.test.ts | 96 +++++++++++ .../validators/document/document-validator.ts | 2 +- .../document/document-validator.unit.test.ts | 156 ++++-------------- .../Form/DynamicForm/DynamicForm.tsx | 140 +++++++++------- .../DynamicForm/DynamicForm.unit.test.tsx | 79 ++++++++- .../controls/SubmitButton/SubmitButton.tsx | 19 ++- .../SubmitButton/SubmitButton.unit.test.tsx | 50 +++++- .../hooks/external/useElement/useElement.ts | 5 +- .../useElement/useElement.unit.test.ts | 28 +++- .../hooks/external/useField/useField.ts | 6 +- .../external/useField/useField.unit.test.ts | 30 +++- .../useFieldHelpers/useFieldHelpers.ts | 9 +- .../organisms/Form/DynamicForm/index.ts | 1 + .../organisms/Form/DynamicForm/types/index.ts | 5 +- .../Form/DynamicForm/utils/format-string.ts | 7 +- .../utils/format-string.unit.test.ts | 25 ++- .../organisms/Form/Validator/types/index.ts | 3 +- .../Form/Validator/validators/index.ts | 2 + .../Form/hooks/useRuleEngine/index.ts | 2 + .../ui/src/components/organisms/Form/index.ts | 1 + 54 files changed, 1455 insertions(+), 221 deletions(-) create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/PluginsRunner.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/PluginsRunner.unit.test.tsx create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/context.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/types.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/usePlugins.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/usePlugins.unit.test.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/usePluginsSubscribe.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/usePluginsSubscribe.unit.test.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginListeners.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginListeners.unit.test.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginsRunner.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginsRunner.unit.test.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins.repository.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins.repository.unit.test.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/event.plugin.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/event.plugin.unit.test.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/ocr.plugin.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/types.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/types.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/useAppMetadata.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/helpers.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/index.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginRunners.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginRunners.unit.test.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginsHandler.ts create mode 100644 apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginsHandler.unit.test.ts diff --git a/apps/kyb-app/src/domains/collection-flow/types/index.ts b/apps/kyb-app/src/domains/collection-flow/types/index.ts index b845067117..c8202f840f 100644 --- a/apps/kyb-app/src/domains/collection-flow/types/index.ts +++ b/apps/kyb-app/src/domains/collection-flow/types/index.ts @@ -1,5 +1,6 @@ import { ITheme } from '@/common/types/settings'; import { Action, Rule } from '@/domains/collection-flow/types/ui-schema.types'; +import { IPlugin } from '@/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/types'; import { AnyObject, IFormElement } from '@ballerine/ui'; import { RJSFSchema, UiSchema } from '@rjsf/utils'; import { CollectionFlowConfig } from './flow-context.types'; @@ -129,6 +130,7 @@ export interface UIPage { number: number; stateName: string; elements: Array>; + plugins: IPlugin[]; actions: Action[]; pageValidation?: Rule[]; } diff --git a/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx b/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx index 6768c5cb42..d7b9858723 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx +++ b/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx @@ -30,6 +30,7 @@ import { setStepCompletionState, } from '@ballerine/common'; import { CollectionFlowUI } from './components/organisms/CollectionFlowUI'; +import { PluginsRunner } from './components/organisms/CollectionFlowUI/components/utility/PluginsRunner'; import { FailedScreen } from './components/pages/FailedScreen'; import { useAdditionalWorkflowContext } from './hooks/useAdditionalWorkflowContext'; @@ -272,10 +273,12 @@ export const CollectionFlow = withSessionProtected(() => {
- + + +
diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx index 8adc8d61a8..96cd0bc9f5 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/CollectionFlowUI.tsx @@ -1,12 +1,19 @@ import './validator'; -import { DynamicFormV2, IFormElement } from '@ballerine/ui'; -import { FunctionComponent } from 'react'; +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 { FunctionComponent, useCallback, useMemo, useRef } from 'react'; +import { usePluginsSubscribe } from './components/utility/PluginsRunner'; +import { usePlugins } from './components/utility/PluginsRunner/hooks/external/usePlugins'; +import { TPluginListener } from './components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginListeners'; +import { useAppMetadata } from './hooks/useAppMetadata'; +import { usePluginsHandler } from './hooks/usePluginsHandler/usePluginsHandler'; import { formElementsExtends } from './ui-elemenets.extends'; -interface ICollectionFlowUIProps { +interface ICollectionFlowUIProps { elements: Array>; - context: object; + context: TValues; } const validationParams = { @@ -18,12 +25,57 @@ export const CollectionFlowUI: FunctionComponent = ({ elements, context, }) => { + const { stateApi } = useStateManagerContext(); + const { handleEvent } = usePluginsHandler(); + const appMetadata = useAppMetadata(); + const { pluginStatuses } = usePlugins(); + + const formRef = useRef(null); + const handlePluginExecution: TPluginListener = useCallback( + (result, _, __, status) => { + if (status === 'completed') { + console.log({ _RESULT: result }); + formRef.current?.setValues(structuredClone(result) as object); + } + }, + [formRef], + ); + + usePluginsSubscribe(handlePluginExecution); + + const metadata = useMemo( + () => ({ + _app: appMetadata, + _plugins: pluginStatuses, + }), + [appMetadata, pluginStatuses], + ); + + const handleChange = useCallback( + (values: CollectionFlowContext) => { + stateApi.setContext(values); + }, + [stateApi], + ); + + const handleSubmit = useCallback(() => { + handleEvent('onSubmit'); + }, [handleEvent]); + + console.log('context', context); + console.log(metadata); + return ( void} + onEvent={handleEvent} + onSubmit={handleSubmit} validationParams={validationParams} + metadata={metadata} + ref={formRef} /> ); }; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/PluginsRunner.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/PluginsRunner.tsx new file mode 100644 index 0000000000..2ae61027c6 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/PluginsRunner.tsx @@ -0,0 +1,15 @@ +import { FunctionComponent } from 'react'; +import { PluginsRunnerContext } from './context'; +import { usePluginsRunner } from './hooks/internal/usePluginsRunner'; +import { IPlugin } from './types'; + +interface IPluginRunnerProps { + plugins: Array>; + children: React.ReactNode | React.ReactNode[]; +} + +export const PluginsRunner: FunctionComponent = ({ plugins, children }) => { + const context = usePluginsRunner(plugins); + + return {children}; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/PluginsRunner.unit.test.tsx b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/PluginsRunner.unit.test.tsx new file mode 100644 index 0000000000..81f308a4ad --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/PluginsRunner.unit.test.tsx @@ -0,0 +1,67 @@ +import { render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, it, vi } from 'vitest'; +import { PluginsRunner } from './PluginsRunner'; +import { usePluginsRunner } from './hooks/internal/usePluginsRunner'; +import { IPlugin } from './types'; + +vi.mock('./hooks/internal/usePluginsRunner', () => ({ + usePluginsRunner: vi.fn(), +})); + +describe('PluginsRunner', () => { + const mockPlugins: Array> = [ + { + name: 'testPlugin', + runOn: [], + params: {}, + }, + ]; + + beforeEach(() => { + vi.mocked(usePluginsRunner).mockReturnValue({ + pluginStatuses: {}, + runPlugin: vi.fn(), + plugins: mockPlugins, + addListener: vi.fn(), + removeListener: vi.fn(), + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render children', () => { + render( + +
Test Child
+
, + ); + + expect(screen.getByTestId('test-child')).toBeInTheDocument(); + }); + + it('should call usePluginsRunner with provided plugins', () => { + render( + +
Test Child
+
, + ); + + expect(usePluginsRunner).toHaveBeenCalledWith(mockPlugins); + }); + + it('should provide context through PluginsRunnerContext', () => { + const TestConsumer = () => { + return
Test Consumer
; + }; + + render( + + + , + ); + + expect(screen.getByTestId('test-consumer')).toBeInTheDocument(); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/context.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/context.ts new file mode 100644 index 0000000000..f66510bfa6 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/context.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import { IPluginsRunnerContext } from './types'; + +export const PluginsRunnerContext = createContext({} as IPluginsRunnerContext); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/index.ts new file mode 100644 index 0000000000..58ab7be8e9 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/index.ts @@ -0,0 +1,2 @@ +export * from './context'; +export * from './types'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/types.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/types.ts new file mode 100644 index 0000000000..1f62130c4d --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/context/types.ts @@ -0,0 +1,22 @@ +import { AnyRecord } from '@ballerine/common'; +import { TPluginListener } from '../hooks/internal/usePluginsRunner/usePluginListeners'; +import { IPlugin } from '../types'; + +export type TPluginStatus = 'pending' | 'running' | 'completed' | 'failed'; + +export interface IPluginStatus { + name: string; + status: TPluginStatus; +} + +export interface IPluginStatuses { + [pluginName: string]: IPluginStatus; +} + +export interface IPluginsRunnerContext { + pluginStatuses: IPluginStatuses; + plugins: IPlugin[]; + runPlugin: (plugin: IPlugin, context: AnyRecord) => Promise; + addListener: (listener: TPluginListener) => void; + removeListener: (listener: TPluginListener) => void; +} diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/index.ts new file mode 100644 index 0000000000..59bc70fd45 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/index.ts @@ -0,0 +1 @@ +export * from './usePlugins'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/usePlugins.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/usePlugins.ts new file mode 100644 index 0000000000..d4d61997c0 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/usePlugins.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { PluginsRunnerContext } from '../../../context'; + +export const usePlugins = () => useContext(PluginsRunnerContext); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/usePlugins.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/usePlugins.unit.test.ts new file mode 100644 index 0000000000..0ac0544982 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePlugins/usePlugins.unit.test.ts @@ -0,0 +1,30 @@ +import { useContext } from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { PluginsRunnerContext } from '../../../context'; +import { usePlugins } from './usePlugins'; + +vi.mock('react', () => ({ + createContext: vi.fn(), + useContext: vi.fn(), +})); + +describe('usePlugins', () => { + it('should call useContext with PluginsRunnerContext', () => { + const mockUseContext = vi.mocked(useContext); + + usePlugins(); + + expect(mockUseContext).toHaveBeenCalledTimes(1); + expect(mockUseContext).toHaveBeenCalledWith(PluginsRunnerContext); + }); + + it('should return the value from useContext', () => { + const mockContextValue = { someValue: 'test' }; + const mockUseContext = vi.mocked(useContext); + mockUseContext.mockReturnValue(mockContextValue); + + const result = usePlugins(); + + expect(result).toBe(mockContextValue); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/index.ts new file mode 100644 index 0000000000..b6face4e87 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/index.ts @@ -0,0 +1 @@ +export * from './usePluginsSubscribe'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/usePluginsSubscribe.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/usePluginsSubscribe.ts new file mode 100644 index 0000000000..2afc9ac4bf --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/usePluginsSubscribe.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; +import { TPluginListener } from '../../internal/usePluginsRunner/usePluginListeners'; +import { usePlugins } from '../usePlugins/usePlugins'; + +export const usePluginsSubscribe = (listener: TPluginListener) => { + const { addListener, removeListener } = usePlugins(); + + useEffect(() => { + addListener(listener); + + return () => { + removeListener(listener); + }; + }, [addListener, listener, removeListener]); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/usePluginsSubscribe.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/usePluginsSubscribe.unit.test.ts new file mode 100644 index 0000000000..3dba2b84cd --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/external/usePluginsSubscribe/usePluginsSubscribe.unit.test.ts @@ -0,0 +1,49 @@ +import { renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { usePlugins } from '../usePlugins/usePlugins'; +import { usePluginsSubscribe } from './usePluginsSubscribe'; + +vi.mock('../usePlugins/usePlugins', () => ({ + usePlugins: vi.fn(), +})); + +describe('usePluginsSubscribe', () => { + const mockAddListener = vi.fn(); + const mockRemoveListener = vi.fn(); + const mockListener = vi.fn(); + + beforeEach(() => { + vi.mocked(usePlugins).mockReturnValue({ + addListener: mockAddListener, + removeListener: mockRemoveListener, + } as any); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should add listener on mount', () => { + renderHook(() => usePluginsSubscribe(mockListener)); + + expect(mockAddListener).toHaveBeenCalledWith(mockListener); + expect(mockAddListener).toHaveBeenCalledTimes(1); + }); + + it('should remove listener on unmount', () => { + const { unmount } = renderHook(() => usePluginsSubscribe(mockListener)); + + unmount(); + + expect(mockRemoveListener).toHaveBeenCalledWith(mockListener); + expect(mockRemoveListener).toHaveBeenCalledTimes(1); + }); + + it('should not add listener multiple times when dependencies change', () => { + const { rerender } = renderHook(() => usePluginsSubscribe(mockListener)); + + rerender(); + + expect(mockAddListener).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/index.ts new file mode 100644 index 0000000000..c50e662a0d --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/index.ts @@ -0,0 +1 @@ +export * from './usePluginsRunner'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginListeners.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginListeners.ts new file mode 100644 index 0000000000..7c16f75647 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginListeners.ts @@ -0,0 +1,36 @@ +import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types'; +import { useCallback, useState } from 'react'; +import { TPluginStatus } from '../../../context'; + +export type TPluginListener = ( + result: TContext, + pluginName: string, + pluginParams: TPluginParams, + status: TPluginStatus, +) => void; + +export const usePluginListeners = () => { + const [listeners, setListeners] = useState([]); + + const addListener = useCallback((listener: TPluginListener) => { + setListeners(prev => [...prev, listener]); + }, []); + + const removeListener = useCallback((listener: TPluginListener) => { + setListeners(prev => prev.filter(l => l !== listener)); + }, []); + + const notifyListeners = useCallback( + ( + result: CollectionFlowContext, + pluginName: string, + pluginParams: unknown, + status: TPluginStatus, + ) => { + listeners.forEach(listener => listener(result, pluginName, pluginParams, status)); + }, + [listeners], + ); + + return { listeners, addListener, removeListener, notifyListeners }; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginListeners.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginListeners.unit.test.ts new file mode 100644 index 0000000000..ad3fea3c38 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginListeners.unit.test.ts @@ -0,0 +1,89 @@ +import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types'; +import { act, renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { TPluginStatus } from '../../../context'; +import { usePluginListeners } from './usePluginListeners'; + +describe('usePluginListeners', () => { + it('should initialize with empty listeners array', () => { + const { result } = renderHook(() => usePluginListeners()); + expect(result.current.listeners).toEqual([]); + }); + + it('should add listener', () => { + const { result } = renderHook(() => usePluginListeners()); + const mockListener = vi.fn(); + + act(() => { + result.current.addListener(mockListener); + }); + + expect(result.current.listeners).toHaveLength(1); + expect(result.current.listeners[0]).toBe(mockListener); + }); + + it('should remove listener', () => { + const { result } = renderHook(() => usePluginListeners()); + const mockListener = vi.fn(); + + act(() => { + result.current.addListener(mockListener); + }); + + act(() => { + result.current.removeListener(mockListener); + }); + + expect(result.current.listeners).toHaveLength(0); + }); + + it('should notify all listeners', () => { + const { result } = renderHook(() => usePluginListeners()); + const mockListener1 = vi.fn(); + const mockListener2 = vi.fn(); + + const testContext = {} as CollectionFlowContext; + const testPluginName = 'testPlugin'; + const testParams = { foo: 'bar' }; + const testStatus: TPluginStatus = 'completed'; + + act(() => { + result.current.addListener(mockListener1); + result.current.addListener(mockListener2); + }); + + act(() => { + result.current.notifyListeners(testContext, testPluginName, testParams, testStatus); + }); + + expect(mockListener1).toHaveBeenCalledWith(testContext, testPluginName, testParams, testStatus); + expect(mockListener2).toHaveBeenCalledWith(testContext, testPluginName, testParams, testStatus); + expect(mockListener1).toHaveBeenCalledTimes(1); + expect(mockListener2).toHaveBeenCalledTimes(1); + }); + + it('should not notify removed listeners', () => { + const { result } = renderHook(() => usePluginListeners()); + const mockListener1 = vi.fn(); + const mockListener2 = vi.fn(); + + const testContext = {} as CollectionFlowContext; + const testPluginName = 'testPlugin'; + const testParams = { foo: 'bar' }; + const testStatus: TPluginStatus = 'completed'; + + act(() => { + result.current.addListener(mockListener1); + result.current.addListener(mockListener2); + result.current.removeListener(mockListener1); + }); + + act(() => { + result.current.notifyListeners(testContext, testPluginName, testParams, testStatus); + }); + + expect(mockListener1).not.toHaveBeenCalled(); + expect(mockListener2).toHaveBeenCalledWith(testContext, testPluginName, testParams, testStatus); + expect(mockListener2).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginsRunner.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginsRunner.ts new file mode 100644 index 0000000000..0e80ade1aa --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginsRunner.ts @@ -0,0 +1,104 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types'; +import { useCallback, useState } from 'react'; +import { IPluginStatuses } from '../../../context'; +import { getPlugin } from '../../../plugins.repository'; +import { IPlugin } from '../../../types'; +import { usePluginListeners } from './usePluginListeners'; + +export const usePluginsRunner = (plugins: Array> = []) => { + const { stateApi } = useStateManagerContext(); + const { notifyListeners, addListener, removeListener } = usePluginListeners(); + const [pluginStatuses, setPluginStatuses] = useState({}); + + const schedulePlugin = useCallback( + (pluginName: string) => { + return new Promise(resolve => { + if (!plugins.find(plugin => plugin.name === pluginName)) { + console.log('Plugin not found', pluginName); + + throw Error('Plugin not found'); + } + + console.log('Scheduling plugin', pluginName); + + setPluginStatuses(prev => { + const plugins = { + ...prev, + [pluginName]: { name: pluginName, status: 'pending' }, + } as const; + + console.log(`Plugin ${pluginName} is pending`); + + return plugins; + }); + + resolve(pluginName); + }); + }, + [plugins], + ); + + const invokePlugin = useCallback( + async (pluginName: string, pluginParams: any) => { + console.log('Invoking plugin', pluginName); + + setPluginStatuses(prev => ({ + ...prev, + [pluginName]: { name: pluginName, status: 'running' }, + })); + + console.log(`Plugin ${pluginName} is running`); + + notifyListeners(stateApi.getContext(), pluginName, pluginParams, 'running'); + + try { + const plugin = getPlugin(pluginName); + const pluginExecutionResult = await plugin( + stateApi.getContext(), + { api: stateApi }, + pluginParams, + ); + + setPluginStatuses(prev => ({ + ...prev, + [pluginName]: { name: pluginName, status: 'completed' }, + })); + + notifyListeners( + pluginExecutionResult as CollectionFlowContext, + pluginName, + pluginParams, + 'completed', + ); + + console.log(`Plugin ${pluginName} is completed`); + } catch (error) { + console.log('Failed to invoke plugin', error); + + setPluginStatuses(prev => ({ + ...prev, + [pluginName]: { name: pluginName, status: 'failed' }, + })); + + notifyListeners(stateApi.getContext(), pluginName, pluginParams, 'failed'); + + console.log(`Plugin ${pluginName} is failed`); + } + }, + [stateApi, notifyListeners], + ); + + const runPlugin = async (plugin: IPlugin) => { + try { + await schedulePlugin(plugin.name); + await invokePlugin(plugin.name, plugin.params); + } catch (error) { + console.log('Failed to run plugin', error); + + throw error; + } + }; + + return { pluginStatuses, plugins, runPlugin, addListener, removeListener }; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginsRunner.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginsRunner.unit.test.ts new file mode 100644 index 0000000000..1610bf8eb6 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/hooks/internal/usePluginsRunner/usePluginsRunner.unit.test.ts @@ -0,0 +1,136 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { act, renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getPlugin } from '../../../plugins.repository'; +import { IPlugin } from '../../../types'; +import { usePluginListeners } from './usePluginListeners'; +import { usePluginsRunner } from './usePluginsRunner'; + +// Mock dependencies +vi.mock('@/components/organisms/DynamicUI/StateManager/components/StateProvider'); +vi.mock('../../../plugins.repository'); +vi.mock('./usePluginListeners'); + +describe('usePluginsRunner', () => { + const mockStateApi = { + getContext: vi.fn(), + }; + + const mockPlugin = vi.fn(); + const mockNotifyListeners = vi.fn(); + const testPlugin = { name: 'test-plugin', params: { param: 'test' } } as IPlugin; + + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(useStateManagerContext).mockReturnValue({ + stateApi: mockStateApi, + payload: {}, + } as any); + + vi.mocked(getPlugin).mockReturnValue(mockPlugin); + vi.mocked(usePluginListeners).mockReturnValue({ + notifyListeners: mockNotifyListeners, + addListener: vi.fn(), + removeListener: vi.fn(), + listeners: [], + }); + }); + + it('should initialize with empty plugin statuses', () => { + const { result } = renderHook(() => usePluginsRunner([testPlugin])); + expect(result.current.pluginStatuses).toEqual({}); + }); + + it('should throw error when plugin is not found', async () => { + const { result } = renderHook(() => usePluginsRunner([])); + + await expect(result.current.runPlugin(testPlugin)).rejects.toThrow('Plugin not found'); + }); + + it('should notify listeners and update plugin status through lifecycle', async () => { + const mockContext = { data: 'test' }; + const mockResult = { data: 'result' }; + vi.mocked(mockStateApi.getContext).mockReturnValue(mockContext); + vi.mocked(mockPlugin).mockResolvedValueOnce(mockResult); + + const { result } = renderHook(() => usePluginsRunner([testPlugin])); + + await act(async () => { + await result.current.runPlugin(testPlugin); + }); + + // Verify status updates and notifications + expect(mockNotifyListeners).toHaveBeenCalledTimes(2); + expect(mockNotifyListeners).toHaveBeenNthCalledWith( + 1, + mockContext, + testPlugin.name, + testPlugin.params, + 'running', + ); + expect(mockNotifyListeners).toHaveBeenNthCalledWith( + 2, + mockResult, + testPlugin.name, + testPlugin.params, + 'completed', + ); + + expect(result.current.pluginStatuses[testPlugin.name]).toEqual({ + name: testPlugin.name, + status: 'completed', + }); + }); + + it('should notify listeners and handle plugin failure', async () => { + const mockContext = { data: 'test' }; + vi.mocked(mockStateApi.getContext).mockReturnValue(mockContext); + vi.mocked(mockPlugin).mockRejectedValueOnce(new Error('Plugin failed')); + + const { result } = renderHook(() => usePluginsRunner([testPlugin])); + + await act(async () => { + try { + await result.current.runPlugin(testPlugin); + } catch (error) { + // Expected error + } + }); + + // Verify failure notifications + expect(mockNotifyListeners).toHaveBeenCalledTimes(2); + expect(mockNotifyListeners).toHaveBeenNthCalledWith( + 1, + mockContext, + testPlugin.name, + testPlugin.params, + 'running', + ); + expect(mockNotifyListeners).toHaveBeenNthCalledWith( + 2, + mockContext, + testPlugin.name, + testPlugin.params, + 'failed', + ); + + expect(result.current.pluginStatuses[testPlugin.name]).toEqual({ + name: testPlugin.name, + status: 'failed', + }); + }); + + it('should call plugin with correct parameters', async () => { + const mockContext = { data: 'test' }; + vi.mocked(mockStateApi.getContext).mockReturnValue(mockContext); + + const { result } = renderHook(() => usePluginsRunner([testPlugin])); + + await act(async () => { + await result.current.runPlugin(testPlugin); + }); + + expect(mockPlugin).toHaveBeenCalledWith(mockContext, { api: mockStateApi }, testPlugin.params); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/index.ts new file mode 100644 index 0000000000..6091968cf2 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/index.ts @@ -0,0 +1,3 @@ +export * from './hooks/external/usePlugins'; +export * from './hooks/external/usePluginsSubscribe'; +export * from './PluginsRunner'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins.repository.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins.repository.ts new file mode 100644 index 0000000000..74520940fc --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins.repository.ts @@ -0,0 +1,17 @@ +import { EVENT_PLUGIN_NAME, eventPlugin } from './plugins/event.plugin'; +import { OCR_PLUGIN_NAME, ocrPlugin } from './plugins/ocr.plugin'; + +export const pluginsRepository = { + [EVENT_PLUGIN_NAME]: eventPlugin, + [OCR_PLUGIN_NAME]: ocrPlugin, +}; + +export const getPlugin = (pluginName: string) => { + const plugin = pluginsRepository[pluginName as keyof typeof pluginsRepository]; + + if (!plugin) { + throw new Error(`Plugin ${pluginName} not found`); + } + + return plugin; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins.repository.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins.repository.unit.test.ts new file mode 100644 index 0000000000..6de5bab956 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins.repository.unit.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import { getPlugin, pluginsRepository } from './plugins.repository'; +import { EVENT_PLUGIN_NAME, eventPlugin } from './plugins/event.plugin'; + +describe('pluginsRepository', () => { + it('should contain the event plugin', () => { + expect(pluginsRepository[EVENT_PLUGIN_NAME]).toBe(eventPlugin); + }); +}); + +describe('getPlugin', () => { + it('should return the correct plugin when given a valid plugin name', () => { + const plugin = getPlugin(EVENT_PLUGIN_NAME); + expect(plugin).toBe(eventPlugin); + }); + + it('should throw an error when given an invalid plugin name', () => { + const invalidPluginName = 'invalid-plugin'; + expect(() => getPlugin(invalidPluginName)).toThrow(`Plugin ${invalidPluginName} not found`); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/event.plugin.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/event.plugin.ts new file mode 100644 index 0000000000..9e7e94ecdb --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/event.plugin.ts @@ -0,0 +1,17 @@ +import { TPluginRunner } from '../types'; + +export interface IEventPluginParams { + eventName: 'NEXT' | 'PREV'; +} + +export const eventPlugin: TPluginRunner = async ( + context, + app, + pluginParams, +) => { + await app.api.sendEvent(pluginParams.eventName); + + return context; +}; + +export const EVENT_PLUGIN_NAME = 'event'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/event.plugin.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/event.plugin.unit.test.ts new file mode 100644 index 0000000000..4409c3697d --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/event.plugin.unit.test.ts @@ -0,0 +1,53 @@ +import { StateMachineAPI } from '@/components/organisms/DynamicUI/StateManager/hooks/useMachineLogic'; +import { describe, expect, it, vi } from 'vitest'; +import { EVENT_PLUGIN_NAME, eventPlugin } from './event.plugin'; + +describe('eventPlugin', () => { + const mockContext = { someData: 'test' }; + const mockApi = { + sendEvent: vi.fn(), + }; + const mockApp = { + api: mockApi as unknown as StateMachineAPI, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should call api.sendEvent with the provided eventName', async () => { + const pluginParams = { + eventName: 'NEXT' as const, + }; + + await eventPlugin(mockContext, mockApp, pluginParams); + + expect(mockApi.sendEvent).toHaveBeenCalledTimes(1); + expect(mockApi.sendEvent).toHaveBeenCalledWith('NEXT'); + }); + + it('should return the unchanged context', async () => { + const pluginParams = { + eventName: 'PREV' as const, + }; + + const result = await eventPlugin(mockContext, mockApp, pluginParams); + + expect(result).toBe(mockContext); + }); + + it('should work with both NEXT and PREV event names', async () => { + const events = ['NEXT', 'PREV'] as const; + + for (const eventName of events) { + await eventPlugin(mockContext, mockApp, { eventName }); + expect(mockApi.sendEvent).toHaveBeenCalledWith(eventName); + } + + expect(mockApi.sendEvent).toHaveBeenCalledTimes(2); + }); + + it('should be defined', () => { + expect(EVENT_PLUGIN_NAME).toBeDefined(); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/ocr.plugin.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/ocr.plugin.ts new file mode 100644 index 0000000000..0f8466d3d7 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/plugins/ocr.plugin.ts @@ -0,0 +1,13 @@ +import { StateMachineAPI } from '@/components/organisms/DynamicUI/StateManager/hooks/useMachineLogic'; +import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types'; + +export const OCR_PLUGIN_NAME = 'ocr'; + +export const ocrPlugin = async ( + context: CollectionFlowContext, + { api }: { api: StateMachineAPI }, +) => { + await api.invokePlugin('fetch_company_information'); + + return api.getContext(); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/types.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/types.ts new file mode 100644 index 0000000000..fe29016770 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/components/utility/PluginsRunner/types.ts @@ -0,0 +1,28 @@ +import { StateMachineAPI } from '@/components/organisms/DynamicUI/StateManager/hooks/useMachineLogic'; +import { AnyObject, IRule, TElementEvent } from '@ballerine/ui'; + +export interface IPluginCommonParams { + debounceTime: number; +} + +export interface IPluginRunOnDefinition { + type: TEvents; + elementId?: string; + rules?: IRule[]; +} + +export interface IPlugin< + TPluginParams extends object = object, + TEvents extends TElementEvent = TElementEvent, +> { + name: string; + runOn: Array>; + params: TPluginParams; + commonParams?: IPluginCommonParams; +} + +export type TPluginRunner = ( + context: TContext, + app: { api: StateMachineAPI }, + pluginParams: TPluginParams, +) => Promise; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/index.ts new file mode 100644 index 0000000000..6268ab9501 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/index.ts @@ -0,0 +1 @@ +export * from './useAppMetadata'; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/types.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/types.ts new file mode 100644 index 0000000000..67d02a010a --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/types.ts @@ -0,0 +1,4 @@ +export interface IAppMetadata { + apiUrl: string; + accessToken: string; +} diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/useAppMetadata.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/useAppMetadata.ts new file mode 100644 index 0000000000..04a2891962 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/useAppMetadata/useAppMetadata.ts @@ -0,0 +1,12 @@ +import { getAccessToken } from '@/helpers/get-access-token.helper'; +import { useMemo } from 'react'; + +export const useAppMetadata = () => { + return useMemo( + () => ({ + apiUrl: import.meta.env.VITE_API_URL, + accessToken: getAccessToken(), + }), + [], + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/helpers.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/helpers.ts new file mode 100644 index 0000000000..86083c0b12 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/helpers.ts @@ -0,0 +1,14 @@ +import { AnyObject, executeRules } from '@ballerine/ui'; +import { IPlugin } from '../../components/utility/PluginsRunner/types'; + +export const checkIfPluginCanRun = ( + runOn: IPlugin['runOn'], + eventName: string, + context: AnyObject, +) => { + const rules = runOn.find(rule => rule.type === eventName); + + if (!rules?.rules?.length) return true; + + return executeRules(context, rules.rules).every(result => result.result); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/index.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginRunners.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginRunners.ts new file mode 100644 index 0000000000..bc99e273e0 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginRunners.ts @@ -0,0 +1,37 @@ +import { useRefValue } from '@/hooks/useRefValue'; +import { AnyObject, IFormEventElement, TElementEvent } from '@ballerine/ui'; +import debounce from 'lodash/debounce'; +import { useCallback, useMemo } from 'react'; +import { usePlugins } from '../../components/utility/PluginsRunner/hooks/external/usePlugins'; +import { IPlugin } from '../../components/utility/PluginsRunner/types'; + +export const usePluginRunners = (plugins: IPlugin[] = []) => { + const { runPlugin } = usePlugins(); + + const runPluginRef = useRefValue(runPlugin); + + const runners = useMemo(() => { + return plugins.map(plugin => ({ + name: plugin.name, + run: debounce((context: AnyObject) => { + void runPluginRef.current(plugin, context); + }, plugin.commonParams?.debounceTime || 0), + runOn: plugin.runOn, + })); + }, [plugins, runPluginRef]); + + const getPluginRunner = useCallback( + (eventName: TElementEvent, element?: IFormEventElement) => { + if (eventName && element) { + return runners.find(runner => + runner.runOn?.some(runOn => runOn.type === eventName && runOn.elementId === element.id), + ); + } + + return runners.find(runner => runner.runOn?.some(runOn => runOn.type === eventName)); + }, + [runners], + ); + + return { runners, getPluginRunner }; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginRunners.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginRunners.unit.test.ts new file mode 100644 index 0000000000..af60e23662 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginRunners.unit.test.ts @@ -0,0 +1,100 @@ +import { IFormEventElement } from '@ballerine/ui'; +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { usePlugins } from '../../components/utility/PluginsRunner/hooks/external/usePlugins'; +import { IPlugin } from '../../components/utility/PluginsRunner/types'; +import { usePluginRunners } from './usePluginRunners'; + +vi.mock('../../components/utility/PluginsRunner/hooks/external/usePlugins'); + +describe('usePluginRunners', () => { + const mockRunPlugin = vi.fn(); + const mockedUsePlugins = vi.mocked(usePlugins); + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + mockedUsePlugins.mockReturnValue({ + runPlugin: mockRunPlugin, + plugins: [], + pluginStatuses: {}, + addListener: vi.fn(), + removeListener: vi.fn(), + }); + }); + + it('should create runners from plugins', () => { + const plugins = [ + { + name: 'test-plugin', + runOn: [{ type: 'onChange', elementId: 'test-id' }], + commonParams: { debounceTime: 100 }, + }, + ]; + + const { result } = renderHook(() => usePluginRunners(plugins as IPlugin[])); + + expect(result.current.runners).toHaveLength(1); + expect(result.current.runners[0]).toMatchObject({ + name: 'test-plugin', + runOn: plugins[0]?.runOn, + }); + }); + + it('should find plugin runner by event name and element', () => { + const plugins = [ + { + name: 'test-plugin', + runOn: [{ type: 'onChange', elementId: 'test-id' }], + }, + ]; + + const { result } = renderHook(() => usePluginRunners(plugins as IPlugin[])); + + const runner = result.current.getPluginRunner('onChange', { + id: 'test-id', + } as IFormEventElement); + + expect(runner).toBeDefined(); + expect(runner?.name).toBe('test-plugin'); + }); + + it('should return undefined when no matching plugin runner found', () => { + const plugins = [ + { + name: 'test-plugin', + runOn: [{ type: 'onChange', elementId: 'test-id' }], + }, + ]; + + const { result } = renderHook(() => usePluginRunners(plugins as IPlugin[])); + + const runner = result.current.getPluginRunner('onBlur', { + id: 'different-id', + } as IFormEventElement); + + expect(runner).toBeUndefined(); + }); + + it('should debounce plugin execution', () => { + const plugins = [ + { + name: 'test-plugin', + runOn: [{ type: 'onChange', elementId: 'test-id' }], + commonParams: { debounceTime: 100 }, + }, + ]; + + const { result } = renderHook(() => usePluginRunners(plugins as IPlugin[])); + const context = { someData: 'test' }; + + result.current.runners[0]?.run(context); + result.current.runners[0]?.run(context); + result.current.runners[0]?.run(context); + + vi.advanceTimersByTime(150); + + expect(mockRunPlugin).toHaveBeenCalledTimes(1); + expect(mockRunPlugin).toHaveBeenCalledWith('test-plugin', context); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginsHandler.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginsHandler.ts new file mode 100644 index 0000000000..1fed9ea105 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginsHandler.ts @@ -0,0 +1,39 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { IFormEventElement, TElementEvent } from '@ballerine/ui'; +import { useCallback } from 'react'; +import { usePlugins } from '../../components/utility/PluginsRunner/hooks/external/usePlugins'; +import { checkIfPluginCanRun } from './helpers'; +import { usePluginRunners } from './usePluginRunners'; + +export const usePluginsHandler = () => { + const { plugins } = usePlugins(); + const { getPluginRunner } = usePluginRunners(plugins); + const { stateApi } = useStateManagerContext(); + + const handleEvent = useCallback( + (eventName: TElementEvent, element?: IFormEventElement) => { + console.log('handleEvent', eventName, element); + const runner = getPluginRunner(eventName, element); + const context = stateApi.getContext(); + + if (!runner) return; + + console.log(`Found plugin ${runner.name} for event ${eventName}`); + + if (!checkIfPluginCanRun(runner.runOn, eventName, context)) { + console.log(`Plugin ${runner.name} cannot run for event ${eventName}`); + + return; + } + + console.log(`Plugin ${runner.name} can run for event ${eventName}`); + + runner.run(context); + }, + [getPluginRunner, stateApi], + ); + + return { + handleEvent, + }; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginsHandler.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginsHandler.unit.test.ts new file mode 100644 index 0000000000..c94cae8cdc --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/hooks/usePluginsHandler/usePluginsHandler.unit.test.ts @@ -0,0 +1,96 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { IFormEventElement } from '@ballerine/ui'; +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { usePlugins } from '../../components/utility/PluginsRunner/hooks/external/usePlugins'; +import { IPlugin } from '../../components/utility/PluginsRunner/types'; +import { checkIfPluginCanRun } from './helpers'; +import { usePluginRunners } from './usePluginRunners'; +import { usePluginsHandler } from './usePluginsHandler'; + +vi.mock('../../components/utility/PluginsRunner/hooks/external/usePlugins'); +vi.mock('./usePluginRunners'); +vi.mock('@/components/organisms/DynamicUI/StateManager/components/StateProvider'); +vi.mock('./helpers'); + +describe('usePluginsHandler', () => { + const mockPlugins = [{ name: 'test-plugin', runOn: [], params: {} }] as IPlugin[]; + const mockGetPluginRunner = vi.fn(); + const mockGetContext = vi.fn(); + const mockRunPlugin = vi.fn(); + + const mockedUsePlugins = vi.mocked(usePlugins); + const mockedUsePluginRunners = vi.mocked(usePluginRunners); + const mockedUseStateManagerContext = vi.mocked(useStateManagerContext); + const mockedCheckIfPluginCanRun = vi.mocked(checkIfPluginCanRun); + + beforeEach(() => { + vi.clearAllMocks(); + + mockedUsePlugins.mockReturnValue({ + plugins: mockPlugins, + runPlugin: vi.fn(), + pluginStatuses: {}, + addListener: vi.fn(), + removeListener: vi.fn(), + }); + + mockedUsePluginRunners.mockReturnValue({ + getPluginRunner: mockGetPluginRunner, + runners: [], + }); + + mockedUseStateManagerContext.mockReturnValue({ + stateApi: { + getContext: mockGetContext, + }, + } as any); + + mockGetContext.mockReturnValue({ someContext: 'value' }); + }); + + it('should not run plugin when no matching runner found', () => { + const { result } = renderHook(() => usePluginsHandler()); + mockGetPluginRunner.mockReturnValue(undefined); + + result.current.handleEvent('onChange', { id: 'test' } as IFormEventElement); + + expect(mockGetPluginRunner).toHaveBeenCalledWith('onChange', { id: 'test' }); + expect(mockRunPlugin).not.toHaveBeenCalled(); + }); + + it('should not run plugin when checkIfPluginCanRun returns false', () => { + const mockRunner = { + name: 'test-plugin', + run: mockRunPlugin, + runOn: [{ type: 'onChange' }], + }; + + const { result } = renderHook(() => usePluginsHandler()); + mockGetPluginRunner.mockReturnValue(mockRunner); + mockedCheckIfPluginCanRun.mockReturnValue(false); + + result.current.handleEvent('onChange', { id: 'test' } as IFormEventElement); + + expect(mockRunPlugin).not.toHaveBeenCalled(); + }); + + it('should run plugin when all conditions are met', () => { + const mockRunner = { + name: 'test-plugin', + run: mockRunPlugin, + runOn: [{ type: 'onChange' }], + }; + const context = { someContext: 'value' }; + + const { result } = renderHook(() => usePluginsHandler()); + mockGetPluginRunner.mockReturnValue(mockRunner); + mockedCheckIfPluginCanRun.mockReturnValue(true); + + result.current.handleEvent('onChange', { id: 'test' } as IFormEventElement); + + expect(mockGetPluginRunner).toHaveBeenCalledWith('onChange', { id: 'test' }); + expect(mockedCheckIfPluginCanRun).toHaveBeenCalledWith(mockRunner.runOn, 'onChange', context); + expect(mockRunPlugin).toHaveBeenCalledWith(context); + }); +}); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts index 210ddce399..9ec5fe7bf3 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.ts @@ -20,7 +20,7 @@ export const documentValidator: TValidator< throw new Error(message); } - const documentValue = document[pageNumber]?.[pageProperty]; + const documentValue = document.pages[pageNumber]?.[pageProperty]; if (!documentValue) { throw new Error(message); diff --git a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts index 1953d082ef..b790fe332c 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts +++ b/apps/kyb-app/src/pages/CollectionFlow/components/organisms/CollectionFlowUI/validators/document/document-validator.unit.test.ts @@ -1,176 +1,80 @@ import { TDocument } from '@ballerine/common'; import { ICommonValidator, TBaseValidators } from '@ballerine/ui'; +import { describe, expect, it } from 'vitest'; import { documentValidator } from './document-validator'; import { IDocumentValidatorParams } from './types'; describe('documentValidator', () => { - const mockParams: ICommonValidator = { + const mockParams = { + message: 'Test message', value: { id: 'test-id', pageNumber: 0, pageProperty: 'ballerineFileId', }, - message: 'Custom error message', - type: 'document', - }; + } as ICommonValidator; - it('should return true for valid document', () => { - const mockDocuments = [ - { - id: 'test-id', - 0: { - ballerineFileId: 'file-123', - }, - propertiesSchema: {}, - }, - ] as TDocument[]; - - expect(documentValidator(mockDocuments, mockParams)).toBe(true); + it('should throw error when value is not an array', () => { + expect(() => documentValidator(null as unknown as TDocument[], mockParams)).toThrow( + 'Document is required', + ); }); - it('should throw error if value is not an array', () => { - expect(() => { - documentValidator(null as any, mockParams); - }).toThrow('Document is required'); + it('should throw error when array is empty', () => { + expect(() => documentValidator([], mockParams)).toThrow('Document is required'); }); - it('should throw error if value is an empty array', () => { - expect(() => { - documentValidator([], mockParams); - }).toThrow('Document is required'); - }); - - it('should throw error if document with specified id is not found', () => { - const mockDocuments = [ - { - id: 'wrong-id', - 0: { - ballerineFileId: 'file-123', - }, - propertiesSchema: {}, - }, - ] as TDocument[]; + it('should throw error when document with specified id is not found', () => { + const mockDocuments = [{ id: 'wrong-id', pages: [] }] as unknown as TDocument[]; - expect(() => { - documentValidator(mockDocuments, mockParams); - }).toThrow('Custom error message'); + expect(() => documentValidator(mockDocuments, mockParams)).toThrow('Test message'); }); - it('should throw error if document page property is not found', () => { - const mockDocuments = [ - { - id: 'test-id', - 0: {}, - propertiesSchema: {}, - }, - ] as TDocument[]; + it('should throw error when document page does not exist', () => { + const mockDocuments = [{ id: 'test-id', pages: [] }] as unknown as TDocument[]; - expect(() => { - documentValidator(mockDocuments, mockParams); - }).toThrow('Custom error message'); + expect(() => documentValidator(mockDocuments, mockParams)).toThrow('Test message'); }); - it('should use default message if not provided', () => { - const paramsWithoutMessage: ICommonValidator< - IDocumentValidatorParams, - TBaseValidators | 'document' - > = { - value: { - id: 'test-id', - pageNumber: 0, - pageProperty: 'ballerineFileId', - }, - type: 'document', - }; - + it('should throw error when document page property does not exist', () => { const mockDocuments = [ { - id: 'wrong-id', - 0: { - ballerineFileId: 'file-123', - }, + id: 'test-id', + pages: [{}], propertiesSchema: {}, }, ] as TDocument[]; - expect(() => { - documentValidator(mockDocuments, paramsWithoutMessage); - }).toThrow('Document is required'); + expect(() => documentValidator(mockDocuments, mockParams)).toThrow('Test message'); }); - it('should use default pageNumber if not provided', () => { - const paramsWithoutPageNumber: ICommonValidator< - IDocumentValidatorParams, - TBaseValidators | 'document' - > = { - value: { - id: 'test-id', - pageProperty: 'ballerineFileId', - }, - message: 'Custom error message', - type: 'document', - }; - + it('should return true for valid document', () => { const mockDocuments = [ { id: 'test-id', - 0: { - ballerineFileId: 'file-123', - }, + pages: [{ ballerineFileId: 'valid-file-id' }], propertiesSchema: {}, }, - ] as TDocument[]; + ] as unknown as TDocument[]; - expect(documentValidator(mockDocuments, paramsWithoutPageNumber)).toBe(true); + expect(documentValidator(mockDocuments, mockParams)).toBe(true); }); - it('should use default pageProperty if not provided', () => { - const paramsWithoutPageProperty: ICommonValidator< - IDocumentValidatorParams, - TBaseValidators | 'document' - > = { - value: { - id: 'test-id', - pageNumber: 0, - }, - message: 'Custom error message', - type: 'document', - }; - + it('should use default values when not provided in params', () => { const mockDocuments = [ { id: 'test-id', - 0: { - ballerineFileId: 'file-123', - }, + pages: [{ ballerineFileId: 'valid-file-id' }], propertiesSchema: {}, }, - ] as TDocument[]; + ] as unknown as TDocument[]; - expect(documentValidator(mockDocuments, paramsWithoutPageProperty)).toBe(true); - }); - - it('should not throw when document value is a File object', () => { - const params: ICommonValidator = { + const minimalParams = { value: { id: 'test-id', - pageNumber: 0, - pageProperty: 'ballerineFileId', - }, - message: 'Custom error message', - type: 'document', - }; - - const mockDocuments = [ - { - id: 'test-id', - 0: { - ballerineFileId: new File([''], 'test.jpg', { type: 'image/jpeg' }), - }, - propertiesSchema: {}, }, - ] as TDocument[]; + } as ICommonValidator; - expect(documentValidator(mockDocuments, params)).toBe(true); + expect(documentValidator(mockDocuments, minimalParams)).toBe(true); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx index e2c253f1c9..c5523dbab5 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent, useMemo } from 'react'; +import { forwardRef, useImperativeHandle, useMemo } from 'react'; import { Renderer, TRendererSchema } from '../../Renderer'; import { ValidatorProvider } from '../Validator'; @@ -12,66 +12,88 @@ import { useValues } from './hooks/internal/useValues'; import { EventsProvider } from './providers/EventsProvider'; import { TaskRunner } from './providers/TaskRunner'; import { extendFieldsRepository, getFieldsRepository } from './repositories'; -import { IDynamicFormProps } from './types'; +import { IDynamicFormProps, IFormRef } from './types'; -export const DynamicFormV2: FunctionComponent = ({ - elements, - values: initialValues, - validationParams = defaultValidationParams, - fieldExtends, - metadata, - onChange, - onFieldChange, - onSubmit, - onEvent, -}) => { - const validationSchema = useValidationSchema(elements); - const valuesApi = useValues({ - values: initialValues, - onChange, - onFieldChange, - }); - const touchedApi = useTouched(elements, valuesApi.values); - const fieldHelpers = useFieldHelpers({ valuesApi, touchedApi }); - const { submit } = useSubmit({ values: valuesApi.values, onSubmit }); +export const DynamicFormV2 = forwardRef( + ( + { + elements, + values: initialValues, + validationParams = defaultValidationParams, + fieldExtends, + metadata, + onChange, + onFieldChange, + onSubmit, + onEvent, + }: IDynamicFormProps, + ref: React.Ref>, + ) => { + const validationSchema = useValidationSchema(elements); + const valuesApi = useValues({ + values: initialValues, + onChange, + onFieldChange, + }); + const touchedApi = useTouched(elements, valuesApi.values); + const fieldHelpers = useFieldHelpers({ valuesApi, touchedApi }); + const { submit } = useSubmit({ values: valuesApi.values, onSubmit }); - const context: IDynamicFormContext = useMemo( - () => ({ - touched: touchedApi.touched, - values: valuesApi.values, + useImperativeHandle(ref, () => ({ submit, - fieldHelpers, - elementsMap: fieldExtends ? extendFieldsRepository(fieldExtends) : getFieldsRepository(), - callbacks: { - onEvent, + validate: () => null, + setValues: valuesApi.setValues, + setTouched: touchedApi.setTouched, + setFieldValue: (fieldName: string, value: unknown) => { + fieldHelpers.setValue(fieldName, fieldName, value); }, - metadata: metadata ?? {}, - validationParams: validationParams ?? {}, - }), - [ - touchedApi.touched, - valuesApi.values, - submit, - fieldHelpers, - fieldExtends, - onEvent, - metadata, - validationParams, - ], - ); + setFieldTouched: fieldHelpers.setTouched, + })); + + const context: IDynamicFormContext = useMemo( + () => ({ + touched: touchedApi.touched, + values: valuesApi.values, + submit, + fieldHelpers, + elementsMap: fieldExtends ? extendFieldsRepository(fieldExtends) : getFieldsRepository(), + callbacks: { + onEvent, + }, + metadata: metadata ?? {}, + validationParams: validationParams ?? {}, + }), + [ + touchedApi.touched, + valuesApi.values, + submit, + fieldHelpers, + fieldExtends, + onEvent, + metadata, + validationParams, + ], + ); + + return ( + + + + + + + + + + ); + }, +); - return ( - - - - - - - - - - ); -}; +DynamicFormV2.displayName = 'DynamicFormV2'; 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 15eda1c5c1..baf56d2723 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 @@ -11,7 +11,7 @@ import { useValidationSchema } from './hooks/internal/useValidationSchema'; import { useValues } from './hooks/internal/useValues'; import { EventsProvider } from './providers/EventsProvider'; import { TaskRunner } from './providers/TaskRunner'; -import { ICommonFieldParams, IDynamicFormProps, IFormElement } from './types'; +import { ICommonFieldParams, IDynamicFormProps, IFormElement, IFormRef } from './types'; // Mock dependencies vi.mock('../../Renderer'); @@ -92,7 +92,7 @@ describe('DynamicFormV2', () => { onSubmit: vi.fn(), onEvent: vi.fn(), metadata: {}, - } as unknown as IDynamicFormProps; + } as unknown as IDynamicFormProps; it('should render without crashing', () => { render(); @@ -224,4 +224,79 @@ describe('DynamicFormV2', () => { expect(providerProps?.value.validationParams).toEqual(customValidationParams); }); + + describe('ref', () => { + const touchedMock = { + touched: { field1: true }, + setTouched: vi.fn(), + setFieldTouched: vi.fn(), + touchAllFields: vi.fn(), + }; + const valuesMock = { + values: { field1: 'value1' }, + setValues: vi.fn(), + setFieldValue: vi.fn(), + }; + const submitMock = { submit: vi.fn() }; + const fieldHelpersMock = { + getTouched: vi.fn(), + getValue: vi.fn(), + setTouched: vi.fn(), + setValue: vi.fn(), + touchAllFields: vi.fn(), + }; + + beforeEach(() => { + vi.mocked(useTouched).mockReturnValue(touchedMock); + vi.mocked(useValues).mockReturnValue(valuesMock); + vi.mocked(useSubmit).mockReturnValue(submitMock); + vi.mocked(useFieldHelpers).mockReturnValue(fieldHelpersMock); + }); + + it('should expose submit method through ref', () => { + const ref = { current: null as IFormRef | null }; + render(); + + expect(ref.current).toHaveProperty('submit', submitMock.submit); + }); + + it('should expose validate method through ref', () => { + const ref = { current: null as IFormRef | null }; + render(); + + expect(ref.current).toHaveProperty('validate'); + expect(ref.current?.validate()).toBeNull(); + }); + + it('should expose setValues method through ref', () => { + const ref = { current: null as IFormRef | null }; + render(); + + expect(ref.current).toHaveProperty('setValues', valuesMock.setValues); + }); + + it('should expose setTouched method through ref', () => { + const ref = { current: null as IFormRef | null }; + render(); + + expect(ref.current).toHaveProperty('setTouched', touchedMock.setTouched); + }); + + it('should expose setFieldValue method through ref', () => { + const ref = { current: null as IFormRef | null }; + render(); + + expect(ref.current).toHaveProperty('setFieldValue'); + + ref.current?.setFieldValue('testField', 'testValue'); + expect(fieldHelpersMock.setValue).toHaveBeenCalledWith('testField', 'testField', 'testValue'); + }); + + it('should expose setFieldTouched method through ref', () => { + const ref = { current: null as IFormRef | null }; + render(); + + expect(ref.current).toHaveProperty('setFieldTouched', fieldHelpersMock.setTouched); + }); + }); }); 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 1a90663dc1..674bf63274 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 @@ -4,6 +4,7 @@ import { useValidator } from '../../../Validator'; import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external/useElement'; import { useField } from '../../hooks/external/useField'; +import { useEvents } from '../../hooks/internal/useEvents'; import { useTaskRunner } from '../../providers/TaskRunner/hooks/useTaskRunner'; import { TDynamicFormElement } from '../../types'; @@ -16,11 +17,12 @@ export const SubmitButton: TDynamicFormElement = ({ const { id } = useElement(element); const { disabled: _disabled } = useField(element); const { fieldHelpers, submit } = useDynamicForm(); - const { runTasks } = useTaskRunner(); + const { runTasks, isRunning } = useTaskRunner(); + const { sendEvent } = useEvents(element); const { touchAllFields } = fieldHelpers; - const { isValid } = useValidator(); + const { isValid, errors } = useValidator(); const { disableWhenFormIsInvalid = false, text = 'Submit' } = element.params || {}; @@ -33,20 +35,27 @@ export const SubmitButton: TDynamicFormElement = ({ const handleSubmit = useCallback(async () => { touchAllFields(); - if (!isValid) return; + if (!isValid) { + console.log(`Submit button clicked but form is invalid`); + console.log('Validation errors', errors); + + return; + } console.log('Starting tasks'); await runTasks(); console.log('Tasks finished'); submit(); - }, [submit, isValid, touchAllFields, runTasks]); + + sendEvent('onSubmit'); + }, [submit, isValid, touchAllFields, runTasks, sendEvent, errors]); return (