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]; +}; diff --git a/packages/haring-react/src/Components/ActionBar/ActionBar.tsx b/packages/haring-react/src/Components/ActionBar/ActionBar.tsx index 46415105..dd973eaa 100644 --- a/packages/haring-react/src/Components/ActionBar/ActionBar.tsx +++ b/packages/haring-react/src/Components/ActionBar/ActionBar.tsx @@ -21,7 +21,9 @@ export function ActionBar>( `${selectedElements} file(s) selected`, ...actionRowOverflowProps } = props; - const numberOfSelectedElements = selectedElements.length; + const numberOfSelectedElements = Array.isArray(selectedElements) + ? selectedElements.length + : [selectedElements].length; return (
diff --git a/packages/haring-react/src/Components/ActionList/ActionList.stories.tsx b/packages/haring-react/src/Components/ActionList/ActionList.stories.tsx index 14caa59f..70d720d2 100644 --- a/packages/haring-react/src/Components/ActionList/ActionList.stories.tsx +++ b/packages/haring-react/src/Components/ActionList/ActionList.stories.tsx @@ -1,4 +1,4 @@ -import type { IActionListAction } from './ActionList'; +import type { IThumbnail } from '../../types'; import type { Meta, StoryObj } from '@storybook/react'; import { ActionList as Cmp } from './ActionList'; @@ -16,19 +16,17 @@ const meta = { type: { name: 'number' }, }, }, - component: Cmp, + component: Cmp, tags: ['autodocs'], title: '3-custom/Components/ActionList', -} satisfies Meta; +} satisfies Meta>; export default meta; type IStory = StoryObj; export const ActionList: IStory = { args: { - actions: actionRowOverflowActionsMock as IActionListAction< - Record - >[], + actions: actionRowOverflowActionsMock, maxVisibleActions: 2, selectedElements: actionRowOverflowSelectedMock, }, diff --git a/packages/haring-react/src/Components/ActionList/ActionList.tsx b/packages/haring-react/src/Components/ActionList/ActionList.tsx index ed0707f2..80871669 100644 --- a/packages/haring-react/src/Components/ActionList/ActionList.tsx +++ b/packages/haring-react/src/Components/ActionList/ActionList.tsx @@ -28,9 +28,9 @@ const defaultTooltipProps = { withArrow: true, }; -export type IActionListAction> = IAction< - Data[] ->; +export type IActionListAction> = + | IAction + | IAction; export interface IActionListProps> extends GroupProps { @@ -41,7 +41,7 @@ export interface IActionListProps> maxVisibleActions?: number; modalProps?: Omit; overflowMenuLabel?: string; - selectedElements: Data[]; + selectedElements: Data | Data[]; } export function ActionList>( @@ -72,7 +72,7 @@ export function ActionList>( children: action.confirmModalProps?.children, confirmColor: action.confirmModalProps?.confirmColor, confirmLabel: action.confirmModalProps?.confirmLabel, - onConfirm: () => action.onAction?.(selectedElements), + onConfirm: () => action.onAction?.(selectedElements as Data & Data[]), title: action.confirmModalProps?.title, }); } @@ -85,7 +85,7 @@ export function ActionList>( clearConfirmAction(); } - function handleModalButton(onAction?: (item: Data[]) => void): void { + function handleModalButton(onAction?: (item: Data | Data[]) => void): void { onAction?.(selectedElements); handleClose(); } @@ -94,7 +94,7 @@ export function ActionList>( if (action.confirmation) { setModal(action); } else { - action.onAction?.(selectedElements); + action.onAction?.(selectedElements as Data & Data[]); } } @@ -103,7 +103,7 @@ export function ActionList>( return ''; } return typeof action.label === 'function' - ? action.label(selectedElements) + ? action.label(selectedElements as Data & Data[]) : action.label; } @@ -112,7 +112,7 @@ export function ActionList>( return null; } return typeof action.icon === 'function' - ? action.icon(selectedElements) + ? action.icon(selectedElements as Data & Data[]) : action.icon; } @@ -123,7 +123,7 @@ export function ActionList>( return undefined; } return typeof action.componentProps === 'function' - ? action.componentProps(selectedElements) + ? action.componentProps(selectedElements as Data & Data[]) : action.componentProps; } diff --git a/packages/haring-react/src/Form/DynamicZone/DynamicZone.mock.tsx b/packages/haring-react/src/Form/DynamicZone/DynamicZone.mock.tsx new file mode 100644 index 00000000..bf06c977 --- /dev/null +++ b/packages/haring-react/src/Form/DynamicZone/DynamicZone.mock.tsx @@ -0,0 +1,87 @@ +import type { IDynamicZoneBlockReference } from './DynamicZoneBlock/DynamicZoneBlock'; +import type { IBaseBlock, IBaseBlockButton } from '../../types'; +import type { IAction } from '@smile/haring-react-shared'; + +import { + Alien, + ArrowDown, + ArrowUp, + Leaf, + Trash, + TreasureChest, +} from '@phosphor-icons/react'; +import { action } from '@storybook/addon-actions'; + +const dynamicZoneBlockActionsMock: IAction[] = [ + { + color: 'white', + icon: , + id: 'move-up', + label: 'Move Up', + onAction: action('Move block up'), + }, + { + color: 'white', + icon: , + id: 'move-down', + label: 'Move Down', + onAction: action('Move block down'), + }, + { + color: 'white', + icon: , + id: 'delete', + label: 'Delete', + onAction: action('Delete block'), + }, +]; + +export const dynamicZoneBlocks: IBaseBlock[] = [ + { + blockActions: dynamicZoneBlockActionsMock, + blockHeader: ( + <> + + First + + ), + blockType: 'default', + id: '1', + opened: false, + value: 'initial', + }, + { + blockActions: dynamicZoneBlockActionsMock, + blockFooter: 'footer', + blockHeader: ( + <> + + Second + + ), + blockType: 'default', + id: '2', + opened: true, + value: 'initial', + }, + { + blockActions: dynamicZoneBlockActionsMock, + blockFooter: 'footer', + blockHeader: ( + <> + + Third + + ), + blockType: 'default', + id: '3', + opened: false, + value: 'initial', + }, +]; + +export const dynamicZoneButtons: IBaseBlockButton[] = [ + { blockType: 'default', label: 'Default', leftSection: }, + { blockType: 'other', label: 'Other', leftSection: }, + { blockType: 'stuff', label: 'Stuff', leftSection: }, +]; diff --git a/packages/haring-react/src/Form/DynamicZone/DynamicZone.module.css b/packages/haring-react/src/Form/DynamicZone/DynamicZone.module.css new file mode 100644 index 00000000..93edbb47 --- /dev/null +++ b/packages/haring-react/src/Form/DynamicZone/DynamicZone.module.css @@ -0,0 +1,12 @@ +.buttonsContainer { + border-radius: 0.5rem; + border: 1px dashed black; +} + +.buttonsLabel { + &:empty { + display: none; + } + + margin-bottom: 10px; +} diff --git a/packages/haring-react/src/Form/DynamicZone/DynamicZone.stories.tsx b/packages/haring-react/src/Form/DynamicZone/DynamicZone.stories.tsx new file mode 100644 index 00000000..e3e9754d --- /dev/null +++ b/packages/haring-react/src/Form/DynamicZone/DynamicZone.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { action } from '@storybook/addon-actions'; + +import { DynamicZone as Cmp } from './DynamicZone'; +import { dynamicZoneBlocks, dynamicZoneButtons } from './DynamicZone.mock'; + +const meta = { + component: Cmp, + tags: ['autodocs'], + title: '3-custom/Form/DynamicZone', +} satisfies Meta; + +export default meta; +type IStory = StoryObj; + +export const DynamicZone: IStory = { + args: { + blockOptions: dynamicZoneButtons, + blocks: dynamicZoneBlocks, + buttonsText: 'Ajouter un block', + internalBlockCardProps: { + headerCardSectionProps: { + bg: 'cadetblue', + c: 'white', + }, + toggleComponentProps: { + actionIconProps: { color: 'white', variant: 'subtle' }, + }, + }, + onAppendBlock: action('onAppendBlock, id'), + onRenderBlockContent: (_b, index) => , + onToggleBlock: action('onToggleBlock'), + }, +}; diff --git a/packages/haring-react/src/Form/DynamicZone/DynamicZone.test.tsx b/packages/haring-react/src/Form/DynamicZone/DynamicZone.test.tsx new file mode 100644 index 00000000..7f63d724 --- /dev/null +++ b/packages/haring-react/src/Form/DynamicZone/DynamicZone.test.tsx @@ -0,0 +1,27 @@ +import type { IBaseBlock } from '../../types'; +import type { ReactElement } from 'react'; + +import { renderWithProviders } from '@smile/haring-react-shared/test-utils'; +import { action } from '@storybook/addon-actions'; +import { expect } from '@storybook/jest'; + +import { DynamicZone } from './DynamicZone'; +import { dynamicZoneBlocks, dynamicZoneButtons } from './DynamicZone.mock'; + +describe('DynamicZone', () => { + it('matches snapshot', () => { + const onRender = (_b: IBaseBlock, index: number): ReactElement => ( + + ); + const { container } = renderWithProviders( + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/haring-react/src/Form/DynamicZone/DynamicZone.tsx b/packages/haring-react/src/Form/DynamicZone/DynamicZone.tsx new file mode 100644 index 00000000..db2b754f --- /dev/null +++ b/packages/haring-react/src/Form/DynamicZone/DynamicZone.tsx @@ -0,0 +1,103 @@ +import type { IDynamicZoneBlockInternalComponentProps } from './DynamicZoneBlock/DynamicZoneBlock'; +import type { IBaseBlock, IBaseBlockButton, IBaseBlockType } from '../../types'; +import type { + CardProps, + ContainerProps, + GroupProps, + StackProps, + TextProps, +} from '@mantine/core'; +import type { ReactElement } from 'react'; + +import { Button, Container, Group, Stack, Text } from '@mantine/core'; + +import classes from './DynamicZone.module.css'; +import { DynamicZoneBlock } from './DynamicZoneBlock/DynamicZoneBlock'; + +export interface IDynamicZoneProps + extends ContainerProps { + blockCardProps?: CardProps; + blockOptions: IBaseBlockButton[]; + blocks: Block[]; + blocksStackProps?: StackProps; + bottomContainerProps?: ContainerProps; + buttonsGroupProps?: GroupProps; + buttonsText?: string; + buttonsTextProps?: TextProps; + internalBlockCardProps?: IDynamicZoneBlockInternalComponentProps; + onAppendBlock: (blockType: IBaseBlockType) => void; + onRenderBlockContent: (block: Block, index: number) => ReactElement; + onToggleBlock: (block: Block, index: number, opened: boolean) => void; +} + +export function DynamicZone( + props: IDynamicZoneProps, +): ReactElement { + const { + blockCardProps, + blockOptions, + blocks, + blocksStackProps, + bottomContainerProps, + buttonsGroupProps, + buttonsText, + buttonsTextProps, + internalBlockCardProps, + onAppendBlock, + onRenderBlockContent, + onToggleBlock, + ...rootContainerProps + } = props; + + function onAddBlock(blockType: IBaseBlockType): void { + onAppendBlock(blockType); + } + + return ( + + + {blocks.map((block, index) => ( + onToggleBlock(block, index, opened)} + opened={block.opened} + reference={{ arrayLength: blocks.length, id: block.id, index }} + > + {onRenderBlockContent(block, index)} + + ))} + + + + {buttonsText} + + + {blockOptions.map(({ blockType, ...button }) => ( + + ))} + + + + ); +} diff --git a/packages/haring-react/src/Form/DynamicZone/DynamicZoneBlock/DynamicZoneBlock.module.css b/packages/haring-react/src/Form/DynamicZone/DynamicZoneBlock/DynamicZoneBlock.module.css new file mode 100644 index 00000000..1098a3bf --- /dev/null +++ b/packages/haring-react/src/Form/DynamicZone/DynamicZoneBlock/DynamicZoneBlock.module.css @@ -0,0 +1,4 @@ +.header { + cursor: pointer; + user-select: none; +} diff --git a/packages/haring-react/src/Form/DynamicZone/DynamicZoneBlock/DynamicZoneBlock.tsx b/packages/haring-react/src/Form/DynamicZone/DynamicZoneBlock/DynamicZoneBlock.tsx new file mode 100644 index 00000000..b657b694 --- /dev/null +++ b/packages/haring-react/src/Form/DynamicZone/DynamicZoneBlock/DynamicZoneBlock.tsx @@ -0,0 +1,130 @@ +import type { IActionListProps } from '../../../Components/ActionList/ActionList'; +import type { + ActionIconProps, + CardProps, + CardSectionProps, + CollapseProps, + ContainerProps, + GroupProps, +} from '@mantine/core'; +import type { IAction } from '@smile/haring-react-shared'; +import type { ReactElement, ReactNode } from 'react'; + +import { + ActionIcon, + Card, + Collapse, + Container, + Group, + Space, +} from '@mantine/core'; +import { CaretDown, CaretUp } from '@phosphor-icons/react'; + +import { ActionList } from '../../../Components/ActionList/ActionList'; + +import classes from './DynamicZoneBlock.module.css'; + +export interface IDynamicZoneBlockInternalComponentProps { + contentCollapseProps?: CollapseProps; + contentContainerProps?: ContainerProps; + footerCardSectionProps?: CardSectionProps; + headerActionListProps?: IActionListProps; + headerCardSectionProps?: CardSectionProps; + headerGroupProps?: GroupProps; + toggleComponentProps?: IDynamicZoneBlockToggleProps; +} + +export interface IDynamicZoneBlockToggleProps { + actionIconProps?: ActionIconProps; + downIcon?: ReactNode; + upIcon?: ReactNode; +} + +export interface IDynamicZoneBlockReference extends Record { + arrayLength: number; + id: string; + index: number; +} + +export interface IDynamicZoneBlockProps extends CardProps { + actions?: IAction[]; + children: ReactNode; + footerChildren?: ReactNode; + headerChildren: ReactNode; + internalComponentProps?: IDynamicZoneBlockInternalComponentProps; + onToggle: (opened: boolean) => void; + opened: boolean; + reference: IDynamicZoneBlockReference; +} + +export function DynamicZoneBlock(props: IDynamicZoneBlockProps): ReactElement { + const { + actions, + children, + footerChildren, + headerChildren, + internalComponentProps, + onToggle, + opened, + reference, + ...cardProps + } = props; + const toggleProps: IDynamicZoneBlockToggleProps = { + downIcon: , + upIcon: , + ...internalComponentProps?.toggleComponentProps, + }; + + return ( + + + + + onToggle(!opened)} + > + {opened ? toggleProps.downIcon : toggleProps.upIcon} + + + {headerChildren} + + {actions && actions.length > 0 ? ( + + actions={actions} + isCompactStyle + selectedElements={reference} + {...internalComponentProps?.headerActionListProps} + /> + ) : null} + + + + + {children} + + + + {footerChildren} + + + ); +} diff --git a/packages/haring-react/src/Form/DynamicZone/__snapshots__/DynamicZone.test.tsx.snap b/packages/haring-react/src/Form/DynamicZone/__snapshots__/DynamicZone.test.tsx.snap new file mode 100644 index 00000000..7cc3c59d --- /dev/null +++ b/packages/haring-react/src/Form/DynamicZone/__snapshots__/DynamicZone.test.tsx.snap @@ -0,0 +1,601 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DynamicZone matches snapshot 1`] = ` +
+ + +
+
+
+
+
+
+ +
+ + + + First +
+
+ + + +
+
+
+ +
+
+
+
+
+
+ +
+ + + + Second +
+
+ + + +
+
+
+
+
+ +
+
+
+ footer +
+
+
+
+
+
+ +
+ + + + Third +
+
+ + + +
+
+
+ +
+ footer +
+
+
+
+

+

+ + + +
+
+
+
+`; diff --git a/packages/haring-react/src/Layouts/FoldableColumnLayout/__snapshots__/FoldableColumnLayout.test.tsx.snap b/packages/haring-react/src/Layouts/FoldableColumnLayout/__snapshots__/FoldableColumnLayout.test.tsx.snap index 743c9f60..29a507c0 100644 --- a/packages/haring-react/src/Layouts/FoldableColumnLayout/__snapshots__/FoldableColumnLayout.test.tsx.snap +++ b/packages/haring-react/src/Layouts/FoldableColumnLayout/__snapshots__/FoldableColumnLayout.test.tsx.snap @@ -71,6 +71,7 @@ exports[`FoldableColumnLayout matches snapshot 1`] = ` aria-hidden="true" class="m_9307d992 mantine-Switch-track" data-label-position="right" + data-without-labels="true" > { + blockActions?: IAction[]; + blockFooter?: ReactNode; + blockHeader: ReactNode; + blockType: IBaseBlockType; + readonly id: string; + opened: boolean; +} + +export interface IBaseBlockButton extends ButtonProps { + blockType: IBaseBlockType; + label: string; +} diff --git a/packages/haring-react/src/types/index.ts b/packages/haring-react/src/types/index.ts index d4ab7a50..ae5dbd6b 100644 --- a/packages/haring-react/src/types/index.ts +++ b/packages/haring-react/src/types/index.ts @@ -1 +1,2 @@ export * from './thumbnail'; +export * from './dynamic-zone';