From 0ef57a7a528e04c271b493cd217791bb5c2bc1a2 Mon Sep 17 00:00:00 2001 From: Quentin Le Caignec Date: Wed, 4 Sep 2024 16:05:24 +0200 Subject: [PATCH] feat: wip FormDynamicZone --- .../FormDynamicZone/FormDynamicZone.mock.tsx | 7 +- .../FormDynamicZone.stories.tsx | 4 +- .../FormDynamicZone/FormDynamicZone.tsx | 145 +++++++++++++----- .../src/types/form-dynamic-zone.ts | 19 ++- .../haring-react-hook-form/src/types/index.ts | 1 + packages/haring-react-shared/src/index.tsx | 1 + .../haring-react-shared/src/types/index.ts | 1 + .../haring-react-shared/src/types/utility.ts | 3 + 8 files changed, 128 insertions(+), 53 deletions(-) create mode 100644 packages/haring-react-hook-form/src/types/index.ts create mode 100644 packages/haring-react-shared/src/types/utility.ts diff --git a/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.mock.tsx b/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.mock.tsx index 4e2c4d7d..2cb1e356 100644 --- a/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.mock.tsx +++ b/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.mock.tsx @@ -1,8 +1,4 @@ -import type { - IFormDynBlock, - IFormField, - IFormRegisterFunc, -} from '../../types/form-dynamic-zone'; +import type { IFormDynBlock, IFormField, IFormRegisterFunc } from '../../types'; import type { ReactElement } from 'react'; import { Alien, Cube, Leaf, TreasureChest } from '@phosphor-icons/react'; @@ -23,6 +19,7 @@ export const dynamicBlocksMock: IFormDynBlock[] = [ ), blockType: 'default', + opened: true, value: 'initial', }, button: { diff --git a/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.stories.tsx b/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.stories.tsx index 9f3d6205..c0a2d56b 100644 --- a/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.stories.tsx +++ b/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.stories.tsx @@ -21,7 +21,9 @@ function render() { const methods = useForm(); return ( - +
+ +
); }; diff --git a/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.tsx b/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.tsx index 2953839b..19ddef54 100644 --- a/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.tsx +++ b/packages/haring-react-hook-form/src/Components/FormDynamicZone/FormDynamicZone.tsx @@ -1,43 +1,52 @@ -import type { IFormDynBlock, IFormField } from '../../types/form-dynamic-zone'; +import type { + IFormDynBlock, + IFormDynSubmit, + IFormField, + IFormFieldWithoutId, +} from '../../types'; import type { IBaseBlockButton, IBaseBlockType } from '@smile/haring-react'; +import type { IDynamicZoneBlockReference } from '@smile/haring-react/src/Form/DynamicZone/DynamicZoneBlock/DynamicZoneBlock'; +import type { IAction } from '@smile/haring-react-shared'; import type { ReactElement } from 'react'; -import { Flex } from '@mantine/core'; +import { Stack } from '@mantine/core'; +import { ArrowDown, ArrowUp, Trash } from '@phosphor-icons/react'; import { DynamicZone } from '@smile/haring-react'; -import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; +import { useFieldArray, useFormContext } from 'react-hook-form'; + +interface IFormDynamicZoneActionLabels { + deleteLabel: string; + moveDownLabel: string; + moveUpLabel: string; +} + +const defaultActionLabels: IFormDynamicZoneActionLabels = { + deleteLabel: 'Delete', + moveDownLabel: 'Move Down', + moveUpLabel: 'Move Up', +}; export interface IFormDynamicZoneProps { + actionLabels?: IFormDynamicZoneActionLabels; dynamicBlocks: IFormDynBlock[]; dynamicZoneName: string; } export function FormDynamicZone(props: IFormDynamicZoneProps): ReactElement { - const { dynamicBlocks, dynamicZoneName } = props; + const { + dynamicBlocks, + dynamicZoneName, + actionLabels = defaultActionLabels, + } = props; const { control, register, handleSubmit } = - useFormContext[]>>(); + useFormContext>(); const { fields, append, remove, swap, update } = useFieldArray({ control, name: dynamicZoneName, }); - const watched = useWatch({ control, name: dynamicZoneName }); - const blockOptions: IBaseBlockButton[] = dynamicBlocks.map((b) => b.button); - function onAppend(type: IBaseBlockType): void { - const correspondingType = dynamicBlocks.find( - (b) => b.block.blockType === type, - ); - if (correspondingType === undefined) { - throw Error( - `Could not find an IFormDynBlock of blocktype '${type} in given dynamicBlocks'`, - ); - } - append({ - ...correspondingType.block, - }); - } - function renderBlock(block: IFormField, index: number): ReactElement { const correspondingType = dynamicBlocks.find( (b) => b.block.blockType === block.blockType, @@ -55,39 +64,91 @@ export function FormDynamicZone(props: IFormDynamicZoneProps): ReactElement { ); } + function onRemove(ref: IDynamicZoneBlockReference): void { + remove(ref.index); + } + + function onSwap( + ref: IDynamicZoneBlockReference, + direction: 'down' | 'up', + ): void { + const secondIndex = ref.index + (direction === 'up' ? -1 : 1); + if (secondIndex >= 0 && secondIndex < ref.arrayLength) { + swap(ref.index, ref.index + (direction === 'up' ? -1 : 1)); + } + } + function onToggle(block: IFormField, index: number, opened: boolean): void { - update(index, { ...block, opened }); + const { id, ...blockWithoutId } = block; + update(index, { ...blockWithoutId, opened }); + } + + function isMoveDisabled( + ref: IDynamicZoneBlockReference, + direction: 'down' | 'up', + ): boolean { + return direction === 'up' + ? ref.index === 0 + : ref.index === ref.arrayLength - 1; + } + + const formDynamicZoneDefaultActions: IAction[] = [ + { + componentProps: (ref) => ({ disabled: isMoveDisabled(ref, 'up') }), + icon: , + id: 'move-up', + label: actionLabels.moveUpLabel, + onAction: (ref) => onSwap(ref, 'up'), + }, + { + componentProps: (ref) => ({ disabled: isMoveDisabled(ref, 'down') }), + icon: , + id: 'move-down', + label: actionLabels.moveDownLabel, + onAction: (ref) => onSwap(ref, 'down'), + }, + { + icon: , + id: 'delete', + label: actionLabels.deleteLabel, + onAction: onRemove, + }, + ]; + + function onAppend(type: IBaseBlockType): void { + const correspondingType = dynamicBlocks.find( + (b) => b.block.blockType === type, + ); + if (correspondingType === undefined) { + throw Error( + `Could not find an IFormDynBlock of blocktype '${type} in given dynamicBlocks'`, + ); + } + append({ + ...correspondingType.block, + blockActions: formDynamicZoneDefaultActions, + }); } - function onSubmit(data: unknown): void { + function onSubmit(data: IFormDynSubmit): void { console.log(data); } return ( -
- - - - - {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} -
- form container, {dynamicZoneName}: {JSON.stringify(watched, null, 2)} + // eslint-disable-next-line @typescript-eslint/no-misused-promises + + blockOptions={blockOptions} - blocks={fields as IFormField[]} + blocks={fields} + fluid + m={0} onAppendBlock={onAppend} onRenderBlockContent={renderBlock} onToggleBlock={onToggle} /> - - -
+ + + ); } diff --git a/packages/haring-react-hook-form/src/types/form-dynamic-zone.ts b/packages/haring-react-hook-form/src/types/form-dynamic-zone.ts index 8d8f86c6..1c801af8 100644 --- a/packages/haring-react-hook-form/src/types/form-dynamic-zone.ts +++ b/packages/haring-react-hook-form/src/types/form-dynamic-zone.ts @@ -1,22 +1,31 @@ import type { IBaseBlock, IBaseBlockButton } from '@smile/haring-react'; +import type { IOmitRespectIndexSignature } from '@smile/haring-react-shared'; import type { ReactElement } from 'react'; import type { UseFormRegister } from 'react-hook-form/dist/types/form'; export interface IFormField extends IBaseBlock { - // fieldName: string; + // fieldId: string; value?: string; } -export type IFormRegisterFunc = - UseFormRegister[]>>; +export interface IFormFieldWithoutId + extends IOmitRespectIndexSignature { + id?: never; // forbid property "id" from being declared, to never override the internal IBaseBlock.id which is only used by useFieldArray() +} + +export type IFormRegisterFunc< + T extends IFormFieldWithoutId = IFormFieldWithoutId, +> = UseFormRegister>; export interface IFormDynBlock { - block: Omit; + block: IFormFieldWithoutId; button: IBaseBlockButton; renderFunc: ( - block: IBaseBlock, + block: IFormField, index: number, registerFunc: IFormRegisterFunc, registerName: string, ) => ReactElement; } + +export type IFormDynSubmit = Record; diff --git a/packages/haring-react-hook-form/src/types/index.ts b/packages/haring-react-hook-form/src/types/index.ts new file mode 100644 index 00000000..152dcd45 --- /dev/null +++ b/packages/haring-react-hook-form/src/types/index.ts @@ -0,0 +1 @@ +export * from './form-dynamic-zone'; diff --git a/packages/haring-react-shared/src/index.tsx b/packages/haring-react-shared/src/index.tsx index 5068ec88..10d7ac72 100644 --- a/packages/haring-react-shared/src/index.tsx +++ b/packages/haring-react-shared/src/index.tsx @@ -44,6 +44,7 @@ export type { IThemeOverride, IThemes, IFilter, + IOmitRespectIndexSignature, } from './types'; // type exports export { mainTheme, primaryTheme, secondaryTheme, themes } from './theme'; diff --git a/packages/haring-react-shared/src/types/index.ts b/packages/haring-react-shared/src/types/index.ts index a8082bf8..0802b594 100644 --- a/packages/haring-react-shared/src/types/index.ts +++ b/packages/haring-react-shared/src/types/index.ts @@ -3,3 +3,4 @@ export * from './options'; export * from './theme'; export * from './filters'; export * from './items'; +export * from './utility'; diff --git a/packages/haring-react-shared/src/types/utility.ts b/packages/haring-react-shared/src/types/utility.ts new file mode 100644 index 00000000..eaaa9fb2 --- /dev/null +++ b/packages/haring-react-shared/src/types/utility.ts @@ -0,0 +1,3 @@ +export type IOmitRespectIndexSignature = { + [P in keyof T as Exclude]: T[P]; +};