From 0e819eebaa3b8feeb9ce1d1ae1ac37358c383d2e Mon Sep 17 00:00:00 2001 From: Quentin Le Caignec <12102823+QuentinLeCaignec@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:35:44 +0100 Subject: [PATCH] Feat/document list (#80) * feat: added `ActionBar`, `SelectableList` and `DocumentList` refactor: moved action bar from `ThumbnailGrid` into `ActionBar` refactor: updated `SearchPage` --- .changeset/brown-foxes-exist.md | 8 + package-lock.json | 3 + .../Components/ActionBar/ActionBar.mock.tsx | 58 ++ .../ActionBar/ActionBar.stories.tsx | 28 + .../Components/ActionBar/ActionBar.test.tsx | 11 + .../src/Components/ActionBar/ActionBar.tsx | 129 ++++ .../__snapshots__/ActionBar.test.tsx.snap | 90 +++ .../DocumentList/DocumentList.mock.tsx | 73 +++ .../DocumentList/DocumentList.stories.tsx | 26 + .../DocumentList/DocumentList.test.tsx | 23 + .../Components/DocumentList/DocumentList.tsx | 92 +++ .../__snapshots__/DocumentList.test.tsx.snap | 550 ++++++++++++++++++ .../SelectableList/SelectableList.mock.tsx | 70 +++ .../SelectableList/SelectableList.stories.tsx | 23 + .../SelectableList/SelectableList.test.tsx | 19 + .../SelectableList/SelectableList.tsx | 57 ++ .../SelectableList.test.tsx.snap | 464 +++++++++++++++ .../ThumbnailGrid/ThumbnailGrid.tsx | 122 +--- .../__snapshots__/ThumbnailGrid.test.tsx.snap | 2 +- packages/react-front-kit/src/index.tsx | 5 +- packages/storybook-pages/package.json | 3 + .../src/Pages/SearchResults/SearchResults.tsx | 127 ++-- .../storybook-pages/src/Pages/pages.mock.tsx | 86 ++- 23 files changed, 1887 insertions(+), 182 deletions(-) create mode 100644 .changeset/brown-foxes-exist.md create mode 100644 packages/react-front-kit/src/Components/ActionBar/ActionBar.mock.tsx create mode 100644 packages/react-front-kit/src/Components/ActionBar/ActionBar.stories.tsx create mode 100644 packages/react-front-kit/src/Components/ActionBar/ActionBar.test.tsx create mode 100644 packages/react-front-kit/src/Components/ActionBar/ActionBar.tsx create mode 100644 packages/react-front-kit/src/Components/ActionBar/__snapshots__/ActionBar.test.tsx.snap create mode 100644 packages/react-front-kit/src/Components/DocumentList/DocumentList.mock.tsx create mode 100644 packages/react-front-kit/src/Components/DocumentList/DocumentList.stories.tsx create mode 100644 packages/react-front-kit/src/Components/DocumentList/DocumentList.test.tsx create mode 100644 packages/react-front-kit/src/Components/DocumentList/DocumentList.tsx create mode 100644 packages/react-front-kit/src/Components/DocumentList/__snapshots__/DocumentList.test.tsx.snap create mode 100644 packages/react-front-kit/src/Components/SelectableList/SelectableList.mock.tsx create mode 100644 packages/react-front-kit/src/Components/SelectableList/SelectableList.stories.tsx create mode 100644 packages/react-front-kit/src/Components/SelectableList/SelectableList.test.tsx create mode 100644 packages/react-front-kit/src/Components/SelectableList/SelectableList.tsx create mode 100644 packages/react-front-kit/src/Components/SelectableList/__snapshots__/SelectableList.test.tsx.snap diff --git a/.changeset/brown-foxes-exist.md b/.changeset/brown-foxes-exist.md new file mode 100644 index 00000000..f8ce9dee --- /dev/null +++ b/.changeset/brown-foxes-exist.md @@ -0,0 +1,8 @@ +--- +'@smile/react-front-kit': minor +'storybook-pages': minor +--- + +Extracted action bar from `ThumbnailGrid` into new component. +Added `ActionBar`, `SelectableList` and `DocumentList` components. +Updated `SearchPage` example page. diff --git a/package-lock.json b/package-lock.json index 252d2bcb..917266b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35761,6 +35761,9 @@ "@smile/react-front-kit-shared": "*", "@smile/react-front-kit-table": "*" }, + "devDependencies": { + "@storybook/addon-actions": "^7.4.1" + }, "peerDependencies": { "@emotion/react": ">=11", "@mantine/core": "6", diff --git a/packages/react-front-kit/src/Components/ActionBar/ActionBar.mock.tsx b/packages/react-front-kit/src/Components/ActionBar/ActionBar.mock.tsx new file mode 100644 index 00000000..6a0e7365 --- /dev/null +++ b/packages/react-front-kit/src/Components/ActionBar/ActionBar.mock.tsx @@ -0,0 +1,58 @@ +import type { IActionBarProps } from './ActionBar'; +import type { IThumbnail, IThumbnailAction } from '../../types'; + +import { Trash } from '@phosphor-icons/react'; +import { FolderMove } from '@smile/react-front-kit-shared'; +import { action } from '@storybook/addon-actions'; + +export const actionBarActionsMock: IThumbnailAction[] = [ + { + icon: , + id: 'move', + isItemAction: false, + isMassAction: true, + label: 'Move in tree', + onAction: action('Move selected in tree'), + }, + { + color: 'red', + confirmModalProps: { + cancelLabel: 'Abort', + children:

Are you sure ?

, + confirmColor: 'red', + confirmLabel: 'Remove', + title: 'remove files ?', + }, + confirmation: true, + icon: , + id: 'delete', + isItemAction: false, + isMassAction: true, + label: 'Delete', + onAction: action('Delete selected'), + }, +]; + +export const actionBarSelectedMock: IThumbnail[] = [ + { + id: '1', + label: 'Label A', + }, + { + id: '2', + label: 'Label B', + }, + { + id: '3', + label: 'Label C', + }, +]; + +export const actionBarLabelMock = (elements: number): string => + `${elements} selected`; + +export const actionBarMock: IActionBarProps = { + actions: actionBarActionsMock, + selectedElements: actionBarSelectedMock, + selectedElementsLabel: actionBarLabelMock, +}; diff --git a/packages/react-front-kit/src/Components/ActionBar/ActionBar.stories.tsx b/packages/react-front-kit/src/Components/ActionBar/ActionBar.stories.tsx new file mode 100644 index 00000000..70de99e4 --- /dev/null +++ b/packages/react-front-kit/src/Components/ActionBar/ActionBar.stories.tsx @@ -0,0 +1,28 @@ +import type { IActionBarAction } from './ActionBar'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { ActionBar as Cmp } from './ActionBar'; +import { + actionBarActionsMock, + actionBarLabelMock, + actionBarSelectedMock, +} from './ActionBar.mock'; + +const meta = { + component: Cmp, + tags: ['autodocs'], + title: '3-custom/Components/ActionBar', +} satisfies Meta; + +export default meta; +type IStory = StoryObj; + +export const ActionBar: IStory = { + args: { + actions: actionBarActionsMock as IActionBarAction< + Record + >[], + selectedElements: actionBarSelectedMock, + selectedElementsLabel: actionBarLabelMock, + }, +}; diff --git a/packages/react-front-kit/src/Components/ActionBar/ActionBar.test.tsx b/packages/react-front-kit/src/Components/ActionBar/ActionBar.test.tsx new file mode 100644 index 00000000..6b544421 --- /dev/null +++ b/packages/react-front-kit/src/Components/ActionBar/ActionBar.test.tsx @@ -0,0 +1,11 @@ +import { renderWithProviders } from '@smile/react-front-kit-shared/test-utils'; + +import { ActionBar } from './ActionBar'; +import { actionBarMock } from './ActionBar.mock'; + +describe('ActionBar', () => { + it('matches snapshot', () => { + const { container } = renderWithProviders(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-front-kit/src/Components/ActionBar/ActionBar.tsx b/packages/react-front-kit/src/Components/ActionBar/ActionBar.tsx new file mode 100644 index 00000000..30a360e5 --- /dev/null +++ b/packages/react-front-kit/src/Components/ActionBar/ActionBar.tsx @@ -0,0 +1,129 @@ +import type { GroupProps, ModalProps } from '@mantine/core'; +import type { Record } from '@phosphor-icons/react'; +import type { + IAction, + IActionConfirmModalProps, +} from '@smile/react-front-kit-shared'; +import type { ReactElement } from 'react'; + +import { Button, Group } from '@mantine/core'; +import { createStyles } from '@mantine/styles'; +import { useState } from 'react'; + +import { ConfirmModal } from '../ConfirmModal/ConfirmModal'; + +const useStyles = createStyles((theme) => ({ + actionBar: { + alignItems: 'center', + background: theme.fn.primaryColor(), + borderRadius: 4, + color: 'white', + display: 'inline-flex', + justifyContent: 'space-between', + padding: '16px 24px', + width: '100%', + }, +})); + +export type IActionBarAction> = IAction< + Data | Data[] +>; + +export interface IActionBarProps> + extends GroupProps { + actions?: IActionBarAction[]; + modalProps?: Omit; + selectedElements: Data[]; + selectedElementsLabel?: (selectedElements: number) => string; +} + +export function ActionBar>( + props: IActionBarProps, +): ReactElement { + const { + actions, + modalProps, + selectedElements, + selectedElementsLabel = (selectedElements: number) => + `${selectedElements} file(s) selected`, + ...groupProps + } = props; + const [confirmAction, setConfirmAction] = useState | null>(null); + const numberOfSelectedElements = selectedElements.length; + + const { classes } = useStyles(); + + function setModal(action: IAction): void { + setConfirmAction({ + cancelColor: action.confirmModalProps?.cancelColor, + cancelLabel: action.confirmModalProps?.cancelLabel, + children: action.confirmModalProps?.children, + confirmColor: action.confirmModalProps?.confirmColor, + confirmLabel: action.confirmModalProps?.confirmLabel, + onConfirm: () => action.onAction?.(selectedElements), + title: action.confirmModalProps?.title, + }); + } + + function clearConfirmAction(): void { + setConfirmAction(null); + } + + function handleClose(): void { + clearConfirmAction(); + } + + function handleModalButton(onAction?: (item: Data[]) => void): void { + onAction?.(selectedElements); + handleClose(); + } + + function handleGridAction(action: IAction): void { + if (action.confirmation) { + setModal(action); + } else { + action.onAction?.(selectedElements); + } + } + + return ( + <> +
+ {selectedElementsLabel(numberOfSelectedElements)} + {actions && actions.length > 0 ? ( + + {actions.map((action) => ( + + ))} + + ) : null} +
+ handleModalButton(confirmAction?.onCancel)} + onClose={handleClose} + onConfirm={() => handleModalButton(confirmAction?.onConfirm)} + opened={Boolean(confirmAction)} + {...modalProps} + > + {confirmAction?.children} + + + ); +} diff --git a/packages/react-front-kit/src/Components/ActionBar/__snapshots__/ActionBar.test.tsx.snap b/packages/react-front-kit/src/Components/ActionBar/__snapshots__/ActionBar.test.tsx.snap new file mode 100644 index 00000000..05e1c4a0 --- /dev/null +++ b/packages/react-front-kit/src/Components/ActionBar/__snapshots__/ActionBar.test.tsx.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ActionBar matches snapshot 1`] = ` +
+
+ + 3 selected + +
+ + +
+
+
+`; diff --git a/packages/react-front-kit/src/Components/DocumentList/DocumentList.mock.tsx b/packages/react-front-kit/src/Components/DocumentList/DocumentList.mock.tsx new file mode 100644 index 00000000..c0316974 --- /dev/null +++ b/packages/react-front-kit/src/Components/DocumentList/DocumentList.mock.tsx @@ -0,0 +1,73 @@ +import type { IDocument } from '../../../dist'; + +import { Button, Space } from '@mantine/core'; +import { DownloadSimple } from '@phosphor-icons/react'; + +export const documentsMock: IDocument[] = [ + { + author: 'Author Name', + content: ( + <> +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad + minim veniam, quis nostrud exercitation ullamco laboris nisi ut +

+ + + ), + date: 'Published on December 24, 2023', + iconType: 'PDF', + id: 1, + path: '(Customer > 567890456 > Invoices)', + title: 'Random_File.PDF', + }, + { + author: 'Author Name', + content: ( + <> +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad + minim veniam, quis nostrud exercitation ullamco laboris nisi ut +

+ + + ), + date: 'Published on December 24, 2023', + iconType: 'PPT', + id: 2, + path: '(Customer > 567890456 > Invoices)', + title: 'Presentation.PPT', + }, + { + author: 'Author Name', + content: ( + <> +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad + minim veniam, quis nostrud exercitation ullamco laboris nisi ut +

+ + + ), + date: 'Published on December 24, 2023', + iconType: 'PDF', + id: 3, + path: '(Customer > 567890456 > Invoices)', + title: 'Other_random_File.PDF', + }, +]; diff --git a/packages/react-front-kit/src/Components/DocumentList/DocumentList.stories.tsx b/packages/react-front-kit/src/Components/DocumentList/DocumentList.stories.tsx new file mode 100644 index 00000000..01064d05 --- /dev/null +++ b/packages/react-front-kit/src/Components/DocumentList/DocumentList.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { action } from '@storybook/addon-actions'; + +import { actionBarActionsMock } from '../ActionBar/ActionBar.mock'; + +import { DocumentList as Cmp } from './DocumentList'; +import { documentsMock } from './DocumentList.mock'; + +const meta = { + component: Cmp, + tags: ['autodocs'], + title: '3-custom/Components/DocumentList', +} satisfies Meta; + +export default meta; +type IStory = StoryObj; + +export const DocumentList: IStory = { + args: { + actions: actionBarActionsMock, + documents: documentsMock, + onDocumentSelected: action('Document selected'), + selectedDocuments: [documentsMock[1]], + }, +}; diff --git a/packages/react-front-kit/src/Components/DocumentList/DocumentList.test.tsx b/packages/react-front-kit/src/Components/DocumentList/DocumentList.test.tsx new file mode 100644 index 00000000..2cb5e940 --- /dev/null +++ b/packages/react-front-kit/src/Components/DocumentList/DocumentList.test.tsx @@ -0,0 +1,23 @@ +import { renderWithProviders } from '@smile/react-front-kit-shared/test-utils'; + +import { actionBarActionsMock } from '../ActionBar/ActionBar.mock'; + +import { DocumentList } from './DocumentList'; +import { documentsMock } from './DocumentList.mock'; + +describe('DocumentList', () => { + beforeEach(() => { + // Prevent mantine random ID + Math.random = () => 0.42; + }); + it('matches snapshot', () => { + const { container } = renderWithProviders( + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-front-kit/src/Components/DocumentList/DocumentList.tsx b/packages/react-front-kit/src/Components/DocumentList/DocumentList.tsx new file mode 100644 index 00000000..fd92273f --- /dev/null +++ b/packages/react-front-kit/src/Components/DocumentList/DocumentList.tsx @@ -0,0 +1,92 @@ +import type { IActionBarAction, IActionBarProps } from '../ActionBar/ActionBar'; +import type { ISelectableListProps } from '../SelectableList/SelectableList'; +import type { ReactElement, ReactNode } from 'react'; + +import { isNotNullNorEmpty } from '@smile/react-front-kit-shared'; +import { useMemo } from 'react'; + +import { ActionBar } from '../ActionBar/ActionBar'; +import { DocumentCard } from '../DocumentCard/DocumentCard'; +import { SelectableList } from '../SelectableList/SelectableList'; + +export interface IDocument extends Record { + author?: string; + content?: ReactNode; + date?: string; + iconType?: string; + id: number | string; + path?: string; + title?: string; +} + +export interface IDocumentListProps + extends Omit< + ISelectableListProps, + 'children' | 'onSelectChange' | 'selectedIndexes' + > { + actionBarProps?: Omit< + IActionBarProps, + 'actions' | 'selectedElements' + >; + actions?: IActionBarAction[]; + documents: IDocument[]; + onDocumentSelected?: (document: IDocument, isSelected: boolean) => void; + selectedDocuments: IDocument[]; +} + +export function DocumentList(props: IDocumentListProps): ReactElement { + const { + actionBarProps, + actions, + documents, + selectedDocuments, + onDocumentSelected, + ...selectableListProps + } = props; + const selectedIndexes = useMemo( + () => + documents + .map((document, i) => + selectedDocuments.map((selected) => selected.id).includes(document.id) + ? i + : null, + ) + .filter(isNotNullNorEmpty), + [documents, selectedDocuments], + ); + + function handleSelectChange(index: number, isSelected: boolean): void { + onDocumentSelected?.(documents[index], isSelected); + } + + return ( +
+ {selectedDocuments.length > 0 && ( + + actions={actions} + selectedElements={selectedDocuments} + {...actionBarProps} + /> + )} + + {documents.map((document) => ( + + {document.content} + + ))} + +
+ ); +} diff --git a/packages/react-front-kit/src/Components/DocumentList/__snapshots__/DocumentList.test.tsx.snap b/packages/react-front-kit/src/Components/DocumentList/__snapshots__/DocumentList.test.tsx.snap new file mode 100644 index 00000000..38c6f1d9 --- /dev/null +++ b/packages/react-front-kit/src/Components/DocumentList/__snapshots__/DocumentList.test.tsx.snap @@ -0,0 +1,550 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DocumentList matches snapshot 1`] = ` +
+
+
+ + 1 file(s) selected + +
+ + +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+ + Random_File.PDF + + + (Customer > 567890456 > Invoices) + +
+
+ + Published on December 24, 2023 + + + - + + + Author Name + +
+
+
+
+
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut +

+ +
+
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+ + Presentation.PPT + + + (Customer > 567890456 > Invoices) + +
+
+ + Published on December 24, 2023 + + + - + + + Author Name + +
+
+
+
+
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut +

+ +
+
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+ + Other_random_File.PDF + + + (Customer > 567890456 > Invoices) + +
+
+ + Published on December 24, 2023 + + + - + + + Author Name + +
+
+
+
+
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut +

+ +
+
+
+
+
+
+
+`; diff --git a/packages/react-front-kit/src/Components/SelectableList/SelectableList.mock.tsx b/packages/react-front-kit/src/Components/SelectableList/SelectableList.mock.tsx new file mode 100644 index 00000000..c19c9ee1 --- /dev/null +++ b/packages/react-front-kit/src/Components/SelectableList/SelectableList.mock.tsx @@ -0,0 +1,70 @@ +import { Button, Space } from '@mantine/core'; +import { DownloadSimple } from '@phosphor-icons/react'; + +import { DocumentCard } from '../DocumentCard/DocumentCard'; + +export const selectableListElementsMock = [ + + <> +

+ Ceci est une description faite pour cette facture et ajoutée par le + créateur lors de l’import du document dans la GED, en l’absence de + description cet espace est laissé vide... +

+ + +
, + + <> +

+ Ceci est une description faite pour cette facture et ajoutée par le + créateur lors de l’import du document dans la GED, en l’absence de + description cet espace est laissé vide... +

+ + +
, + + <> +

+ Ceci est une description faite pour cette facture et ajoutée par le + créateur lors de l’import du document dans la GED, en l’absence de + description cet espace est laissé vide... +

+ + +
, +]; diff --git a/packages/react-front-kit/src/Components/SelectableList/SelectableList.stories.tsx b/packages/react-front-kit/src/Components/SelectableList/SelectableList.stories.tsx new file mode 100644 index 00000000..59f14cf2 --- /dev/null +++ b/packages/react-front-kit/src/Components/SelectableList/SelectableList.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { action } from '@storybook/addon-actions'; + +import { SelectableList as Cmp } from './SelectableList'; +import { selectableListElementsMock } from './SelectableList.mock'; + +const meta = { + component: Cmp, + tags: ['autodocs'], + title: '3-custom/Components/SelectableList', +} satisfies Meta; + +export default meta; +type IStory = StoryObj; + +export const SelectableList: IStory = { + args: { + children: selectableListElementsMock, + onSelectChange: action('Element selected'), + selectedIndexes: [1, 2], + }, +}; diff --git a/packages/react-front-kit/src/Components/SelectableList/SelectableList.test.tsx b/packages/react-front-kit/src/Components/SelectableList/SelectableList.test.tsx new file mode 100644 index 00000000..bfdae744 --- /dev/null +++ b/packages/react-front-kit/src/Components/SelectableList/SelectableList.test.tsx @@ -0,0 +1,19 @@ +import { renderWithProviders } from '@smile/react-front-kit-shared/test-utils'; + +import { SelectableList } from './SelectableList'; +import { selectableListElementsMock } from './SelectableList.mock'; + +describe('SelectableList', () => { + beforeEach(() => { + // Prevent mantine random ID + Math.random = () => 0.42; + }); + it('matches snapshot', () => { + const { container } = renderWithProviders( + + {selectableListElementsMock} + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-front-kit/src/Components/SelectableList/SelectableList.tsx b/packages/react-front-kit/src/Components/SelectableList/SelectableList.tsx new file mode 100644 index 00000000..5db4e4f6 --- /dev/null +++ b/packages/react-front-kit/src/Components/SelectableList/SelectableList.tsx @@ -0,0 +1,57 @@ +import type { StackProps } from '@mantine/core'; +import type { ReactElement } from 'react'; + +import { Checkbox, Group, Stack } from '@mantine/core'; +import { createStyles } from '@mantine/styles'; + +const useStyles = createStyles(() => ({ + element: { + ':not(:last-child):after': { + border: '1px #e9ecef solid', + bottom: '-41px', + content: '""', + position: 'absolute', + width: '100%', + }, + position: 'relative', + }, +})); + +export interface ISelectableListProps extends StackProps { + children: ReactElement[]; + onSelectChange?: (index: number, isSelected: boolean) => void; + selectedIndexes?: number[]; +} + +export function SelectableList(props: ISelectableListProps): ReactElement { + const { + children, + onSelectChange, + selectedIndexes = [], + ...stackProps + } = props; + + const { classes } = useStyles(); + + return ( + + {children.map((element, index) => ( + // eslint-disable-next-line react/no-array-index-key + + onSelectChange?.(index, e.currentTarget.checked)} + radius={2} + size={16} + /> + {element} + + ))} + + ); +} diff --git a/packages/react-front-kit/src/Components/SelectableList/__snapshots__/SelectableList.test.tsx.snap b/packages/react-front-kit/src/Components/SelectableList/__snapshots__/SelectableList.test.tsx.snap new file mode 100644 index 00000000..fca567e1 --- /dev/null +++ b/packages/react-front-kit/src/Components/SelectableList/__snapshots__/SelectableList.test.tsx.snap @@ -0,0 +1,464 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SelectableList matches snapshot 1`] = ` +
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+ + Random_File.PDF + + + (Customer > 567890456 > Invoices) + +
+
+ + Published on December 24, 2023 + + + - + + + Aline Dupon + +
+
+
+
+
+

+ Ceci est une description faite pour cette facture et ajoutée par le créateur lors de l’import du document dans la GED, en l’absence de description cet espace est laissé vide... +

+ +
+
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+ + Presentation.PPT + + + (Customer > 567890456 > Invoices) + +
+
+ + Published on December 24, 2023 + + + - + + + Julien Dominique + +
+
+
+
+
+

+ Ceci est une description faite pour cette facture et ajoutée par le créateur lors de l’import du document dans la GED, en l’absence de description cet espace est laissé vide... +

+ +
+
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+ + Other_random_File.PDF + + + (Customer > 567890456 > Invoices) + +
+
+ + Published on December 24, 2023 + + + - + + + Mohamed Aldri + +
+
+
+
+
+

+ Ceci est une description faite pour cette facture et ajoutée par le créateur lors de l’import du document dans la GED, en l’absence de description cet espace est laissé vide... +

+ +
+
+
+
+
+
+`; diff --git a/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.tsx b/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.tsx index 5999dea8..682e3127 100644 --- a/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.tsx +++ b/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.tsx @@ -2,39 +2,26 @@ import type { IThumbnail, IThumbnailAction } from '../../types'; import type { SimpleGridProps } from '@mantine/core'; -import type { IActionConfirmModalProps } from '@smile/react-front-kit-shared'; import type { ReactElement } from 'react'; -import { Button, Group, SimpleGrid } from '@mantine/core'; +import { SimpleGrid } from '@mantine/core'; import { createStyles } from '@mantine/styles'; -import { useState } from 'react'; -import { ConfirmModal } from '../ConfirmModal/ConfirmModal'; +import { ActionBar } from '../ActionBar/ActionBar'; import { Thumbnail } from '../Thumbnail/Thumbnail'; -const useStyles = createStyles((theme) => ({ +const useStyles = createStyles(() => ({ container: { display: 'flex', flexDirection: 'column', gap: 24, }, - topBar: { - alignItems: 'center', - background: theme.fn.primaryColor(), - borderRadius: 4, - color: 'white', - display: 'inline-flex', - justifyContent: 'space-between', - padding: '16px 24px', - }, })); function defaultSelectedElementsText(n: number): string { return `${n} selected file${n > 1 ? 's' : ''}`; } -type IGridActionConfirmModalProps = IActionConfirmModalProps; - export interface IThumbnailGridProps extends SimpleGridProps { actions?: IThumbnailAction[]; onThumbnailClick?: (item: IThumbnail, index: number) => void; @@ -54,8 +41,6 @@ export function ThumbnailGrid(props: IThumbnailGridProps): ReactElement { const selectedElements = thumbnails.filter((thumbnail) => thumbnail.selected); const numberOfSelectedElements = selectedElements.length; - const [confirmAction, setConfirmAction] = - useState(null); const massActions = actions.filter(({ isMassAction }) => isMassAction); const itemActions = actions.filter(({ isItemAction = true }) => isItemAction); @@ -65,88 +50,25 @@ export function ThumbnailGrid(props: IThumbnailGridProps): ReactElement { onThumbnailClick?.(thumbnails[index], index); } - function setModal(action: IThumbnailAction): void { - setConfirmAction({ - cancelColor: action.confirmModalProps?.cancelColor, - cancelLabel: action.confirmModalProps?.cancelLabel, - children: action.confirmModalProps?.children, - confirmColor: action.confirmModalProps?.confirmColor, - confirmLabel: action.confirmModalProps?.confirmLabel, - onConfirm: () => action.onAction?.(selectedElements), - title: action.confirmModalProps?.title, - }); - } - - function handleGridAction(action: IThumbnailAction): void { - if (action.confirmation) { - setModal(action); - } else { - action.onAction?.(selectedElements); - } - } - - function clearConfirmAction(): void { - setConfirmAction(null); - } - - function handleClose(): void { - clearConfirmAction(); - } - - function handleModalButton(onAction?: (item: IThumbnail[]) => void): void { - onAction?.(selectedElements); - handleClose(); - } - return ( - <> -
- {numberOfSelectedElements > 0 && ( -
- {selectedElementsText(numberOfSelectedElements)} - {massActions.length > 0 && ( - - {massActions.map((action) => ( - - ))} - - )} -
- )} - - {thumbnails.map((thumbnail, index) => ( - handleSelect(index)} - {...thumbnail} - /> - ))} - -
- handleModalButton(confirmAction?.onCancel)} - onClose={handleClose} - onConfirm={() => handleModalButton(confirmAction?.onConfirm)} - opened={Boolean(confirmAction)} - > - {confirmAction?.children} - - +
+ {numberOfSelectedElements > 0 && ( + + actions={massActions} + selectedElements={selectedElements} + selectedElementsLabel={selectedElementsText} + /> + )} + + {thumbnails.map((thumbnail, index) => ( + handleSelect(index)} + {...thumbnail} + /> + ))} + +
); } diff --git a/packages/react-front-kit/src/Components/ThumbnailGrid/__snapshots__/ThumbnailGrid.test.tsx.snap b/packages/react-front-kit/src/Components/ThumbnailGrid/__snapshots__/ThumbnailGrid.test.tsx.snap index a6678da3..e3ca7158 100644 --- a/packages/react-front-kit/src/Components/ThumbnailGrid/__snapshots__/ThumbnailGrid.test.tsx.snap +++ b/packages/react-front-kit/src/Components/ThumbnailGrid/__snapshots__/ThumbnailGrid.test.tsx.snap @@ -6,7 +6,7 @@ exports[`ThumbnailGrid matches snapshot 1`] = ` class="mantine-1skbp38" >
1 selected file diff --git a/packages/react-front-kit/src/index.tsx b/packages/react-front-kit/src/index.tsx index a7902d96..7d5ca2b0 100644 --- a/packages/react-front-kit/src/index.tsx +++ b/packages/react-front-kit/src/index.tsx @@ -1,14 +1,16 @@ /* eslint-disable react-refresh/only-export-components */ export * from '@smile/react-front-kit-shared'; // component exports -export * from './Components/Filters/Filters'; +export * from './Components/ActionBar/ActionBar'; export * from './Components/BitConverter/BitConverter'; export * from './Components/Breadcrumbs/Breadcrumbs'; export * from './Components/CollapseButton/CollapseButtonControlled'; export * from './Components/CollapseButton/CollapseButton'; export * from './Components/ConfirmModal/ConfirmModal'; export * from './Components/DocumentCard/DocumentCard'; +export * from './Components/DocumentList/DocumentList'; export * from './Components/DropdownButton/DropdownButton'; +export * from './Components/Filters/Filters'; export * from './Components/Header/Header'; export * from './Components/HeaderSearch/HeaderSearch'; export * from './Components/InfoCard/InfoCard'; @@ -16,6 +18,7 @@ export * from './Components/InfoCard/Motif'; export * from './Components/Pagination/Pagination'; export * from './Components/ResponsiveTabs/ResponsiveTabs'; export * from './Components/SearchBar/SearchBar'; +export * from './Components/SelectableList/SelectableList'; export * from './Components/SidebarMenu/SidebarMenu'; export * from './Components/SwitchableView/SwitchableView'; export * from './Components/Thumbnail/Thumbnail'; diff --git a/packages/storybook-pages/package.json b/packages/storybook-pages/package.json index 317512bc..ae42faa2 100644 --- a/packages/storybook-pages/package.json +++ b/packages/storybook-pages/package.json @@ -12,6 +12,9 @@ "@smile/react-front-kit-shared": "*", "@smile/react-front-kit-table": "*" }, + "devDependencies": { + "@storybook/addon-actions": "^7.4.1" + }, "peerDependencies": { "@emotion/react": ">=11", "@mantine/core": "6", diff --git a/packages/storybook-pages/src/Pages/SearchResults/SearchResults.tsx b/packages/storybook-pages/src/Pages/SearchResults/SearchResults.tsx index f760a85d..b70cf8ad 100644 --- a/packages/storybook-pages/src/Pages/SearchResults/SearchResults.tsx +++ b/packages/storybook-pages/src/Pages/SearchResults/SearchResults.tsx @@ -1,23 +1,20 @@ 'use client'; +import type { IDocument } from '@smile/react-front-kit'; import type { ReactElement } from 'react'; import { AppShell, Box, - Button, - Divider, Flex, Paper, Select, - Space, - Stack, createStyles, rem, } from '@mantine/core'; -import { CaretDown, DownloadSimple } from '@phosphor-icons/react'; +import { CaretDown } from '@phosphor-icons/react'; import { - DocumentCard, + DocumentList, FoldableColumnLayout, Header, Motif, @@ -30,7 +27,13 @@ import { } from '@smile/react-front-kit-shared'; import { useState } from 'react'; -import { headerContent, headerLeft, headerRight } from '../pages.mock'; +import { + headerContent, + headerLeft, + headerRight, + searchActions, + searchDocuments, +} from '../pages.mock'; export interface IOption { label: string; @@ -121,8 +124,32 @@ export function SearchResults(): ReactElement { const [activeSorting, setActiveSorting] = useState( sortingOptions[0]?.value, ); - const totalPages = Math.ceil(typeFilteredResults / rowsPerPage); + // Documents + const [selectedDocuments, setSelectedDocuments] = useState([]); + + function handleDocumentSelected( + selectedDocument: IDocument, + isSelected: boolean, + ): void { + const newSelectedDocuments = [...selectedDocuments]; + if ( + newSelectedDocuments + .map((document) => document.id) + .includes(selectedDocument.id) && + !isSelected + ) { + delete newSelectedDocuments[ + newSelectedDocuments.findIndex( + (document) => document.id === selectedDocument.id, + ) + ]; + } else { + newSelectedDocuments.push(selectedDocument); + } + setSelectedDocuments(newSelectedDocuments.filter(isNotNullNorEmpty)); + } + const { classes } = useStyles(); return ( @@ -203,77 +230,19 @@ export function SearchResults(): ReactElement { } topBlockTheme={{ ...secondaryTheme, colorScheme: 'dark' }} > - - - - - <> -

- Ceci est une description faite pour cette facture et ajoutée - par le créateur lors de l’import du document dans la GED, en - l’absence de description cet espace est laissé vide... -

- - -
- - - - - <> -

- Ceci est une description faite pour cette facture et ajoutée - par le créateur lors de l’import du document dans la GED, en - l’absence de description cet espace est laissé vide... -

- - -
- - - - - <> -

- Ceci est une description faite pour cette facture et ajoutée - par le créateur lors de l’import du document dans la GED, en - l’absence de description cet espace est laissé vide... -

- - -
- -
+ + + `${n} fichier${n > 1 ? 's' : ''} sélectionné${ + n > 1 ? 's' : '' + }`, + }} + actions={searchActions} + documents={searchDocuments} + onDocumentSelected={handleDocumentSelected} + selectedDocuments={selectedDocuments} + /> @@ -35,3 +39,83 @@ export const headerRight = ( /> ); + +export const searchDocuments: IDocument[] = [ + { + author: 'Aline Dupon', + content: ( + <> +

+ Ceci est une description faite pour cette facture et ajoutée par le + créateur lors de l’import du document dans la GED, en l’absence de + description cet espace est laissé vide... +

+ + + ), + date: 'Published on December 24, 2023', + iconType: 'PDF', + id: 1, + path: '(Customer > 567890456 > Invoices)', + title: 'Random_File.PDF', + }, + { + author: 'Julien Dominique', + content: ( + <> +

+ Ceci est une description faite pour cette facture et ajoutée par le + créateur lors de l’import du document dans la GED, en l’absence de + description cet espace est laissé vide... +

+ + + ), + date: 'Published on December 24, 2023', + iconType: 'PPT', + id: 2, + path: '(Customer > 567890456 > Invoices)', + title: 'Presentation.PPT', + }, + { + author: 'Mohamed Aldri', + content: ( + <> +

+ Ceci est une description faite pour cette facture et ajoutée par le + créateur lors de l’import du document dans la GED, en l’absence de + description cet espace est laissé vide... +

+ + + ), + date: 'Published on December 24, 2023', + iconType: 'PDF', + id: 3, + path: '(Customer > 567890456 > Invoices)', + title: 'Other_random_File.PDF', + }, +]; + +export const searchActions: IActionBarAction[] = [ + { + icon: , + id: 'download', + isItemAction: false, + isMassAction: true, + label: 'Télécharger', + onAction: action('Download files'), + }, +];