diff --git a/.changeset/giant-planes-juggle.md b/.changeset/giant-planes-juggle.md new file mode 100644 index 00000000..b29cfbb1 --- /dev/null +++ b/.changeset/giant-planes-juggle.md @@ -0,0 +1,7 @@ +--- +'@smile/react-front-kit-shared': minor +'@smile/react-front-kit-table': minor +'@smile/react-front-kit': minor +--- + +Reworked action/confirm types and moved then in `react-front-kit-shared` then refactored types in `Table`, `Thumbnail` and `ConfirmModal` , Added `ThumbnailGrid` and `TableGridView` components, diff --git a/packages/react-front-kit-shared/src/types/actions.ts b/packages/react-front-kit-shared/src/types/actions.ts new file mode 100644 index 00000000..4ac140f6 --- /dev/null +++ b/packages/react-front-kit-shared/src/types/actions.ts @@ -0,0 +1,35 @@ +import type { MantineColor, ModalProps } from '@mantine/core'; +import type { ReactNode } from 'react'; + +export interface IConfirmModal extends Omit { + cancelColor?: MantineColor; + cancelLabel?: string; + confirmColor?: MantineColor; + confirmLabel?: string; + onCancel?: () => void; + onConfirm?: () => void; + title?: string; +} + +export interface IActionConfirmModalProps + extends Omit { + onCancel?: (item: Item) => false | void; + onClose?: () => void; + onConfirm?: (item: Item) => false | void; +} + +export interface IAction { + color?: string; + confirmModalProps?: IActionConfirmModalProps; + confirmation?: boolean; + icon: ReactNode; + id: number | string; + isItemAction?: boolean; + isMassAction?: boolean; + label: string; + onAction?: (item: Item) => void; +} + +export interface IConfirmAction extends IAction { + item: Item; +} diff --git a/packages/react-front-kit-shared/src/types/index.ts b/packages/react-front-kit-shared/src/types/index.ts index 5f30ef38..b914401a 100644 --- a/packages/react-front-kit-shared/src/types/index.ts +++ b/packages/react-front-kit-shared/src/types/index.ts @@ -1 +1,2 @@ +export * from './actions'; export * from './options'; diff --git a/packages/react-front-kit-table/src/Components/Table/Table.mock.tsx b/packages/react-front-kit-table/src/Components/Table/Table.mock.tsx new file mode 100644 index 00000000..e192cfe9 --- /dev/null +++ b/packages/react-front-kit-table/src/Components/Table/Table.mock.tsx @@ -0,0 +1,146 @@ +import type { ITableProps } from './Table'; + +import { + DownloadSimple, + Eye, + PencilSimple, + ShareNetwork, + Star, + Trash, +} from '@phosphor-icons/react'; +import { FolderMove } from '@smile/react-front-kit-shared'; +import { action } from '@storybook/addon-actions'; + +export const tableMock: ITableProps> = { + actions: [ + { + icon: , + id: 'move', + isMassAction: true, + label: 'Move in tree', + onAction: action('Move in tree'), + }, + { + icon: , + id: 'open', + label: 'Open document', + onAction: action('Open document'), + }, + { + icon: , + id: 'edit', + label: 'Edit document', + onAction: action('Edit document'), + }, + { + icon: , + id: 'favorite', + label: 'Add to favorites', + onAction: action('Add to favorites'), + }, + { + confirmation: true, + icon: , + id: 'share', + label: 'Share', + onAction: action('Share'), + }, + { + icon: , + id: 'download', + label: 'Download', + onAction: action('Download'), + }, + { + color: 'red', + confirmModalProps: { + children: 'Are you sure you want to delete ?', + confirmColor: 'red', + confirmLabel: 'Delete', + onCancel: action('Delete:Cancel'), + onConfirm: action('Delete:Confirm'), + title: 'Delete ?', + }, + confirmation: true, + icon: , + id: 'delete', + isMassAction: true, + label: 'Delete', + onAction: action('Delete'), + }, + ], + columns: [ + { + accessorKey: 'id', + header: 'id', + }, + { + accessorKey: 'format', + header: 'Format', + }, + { + accessorKey: 'title', + header: 'Titre', + }, + { + accessorKey: 'creator', + header: 'Créateur', + }, + { + accessorKey: 'date', + header: 'Date publication', + }, + ], + data: [ + { + creator: 'Valentin Perello', + date: '16/05/2022', + format: 'SVG', + id: 1, + title: 'Doc test 1', + }, + { + creator: 'Valentin Perello', + date: '17/05/2022', + format: 'PDF', + id: 2, + title: 'Doc test 2', + }, + { + creator: 'Valentin Perello', + date: '18/05/2022', + format: 'PDF', + id: 3, + title: 'Doc test 3', + }, + { + creator: 'Valentin Perello', + date: '19/05/2022', + format: 'PDF', + id: 4, + title: 'Doc test 4', + }, + { + creator: 'Valentin Perello', + date: '20/05/2022', + format: 'PDF', + id: 5, + title: 'Doc test 5', + }, + { + creator: 'Valentin Perello', + date: '21/05/2022', + format: 'PDF', + id: 6, + title: 'Doc test 6', + }, + { + creator: 'Valentin Perello', + date: '22/05/2022', + format: 'PDF', + id: 7, + title: 'Doc test 7', + }, + ], + rowActionNumber: 3, +}; diff --git a/packages/react-front-kit-table/src/Components/Table/Table.stories.tsx b/packages/react-front-kit-table/src/Components/Table/Table.stories.tsx index f1c9dde2..52ae231e 100644 --- a/packages/react-front-kit-table/src/Components/Table/Table.stories.tsx +++ b/packages/react-front-kit-table/src/Components/Table/Table.stories.tsx @@ -1,17 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { - DownloadSimple, - Eye, - PencilSimple, - ShareNetwork, - Star, - Trash, -} from '@phosphor-icons/react'; -import { FolderMove } from '@smile/react-front-kit-shared'; -import { action } from '@storybook/addon-actions'; - import { Table as Cmp } from './Table'; +import { tableMock } from './Table.mock'; const meta = { component: Cmp, @@ -25,115 +15,6 @@ type IStory = StoryObj; export const Table: IStory = { args: { - actions: [ - { - icon: , - isMassAction: true, - label: 'Move in tree', - onAction: action('Move in tree'), - }, - { - icon: , - label: 'Open document', - onAction: action('Open document'), - }, - { - icon: , - label: 'Edit document', - onAction: action('Edit document'), - }, - { - icon: , - label: 'Add to favorites', - onAction: action('Add to favorites'), - }, - { - confirmation: true, - icon: , - label: 'Share', - onAction: action('Share'), - }, - { - icon: , - label: 'Download', - onAction: action('Download'), - }, - { - color: 'red', - confirmModalProps: { - children: 'Are you sur you want to delete ?', - confirmColor: 'red', - confirmLabel: 'Delete', - onCancel: action('Delete:Cancel'), - onConfirm: action('Delete:Confirm'), - title: 'Delete ?', - }, - confirmation: true, - icon: , - isMassAction: true, - label: 'Delete', - onAction: action('Delete'), - }, - ], - columns: [ - { - accessorKey: 'id', - header: 'id', - }, - { - accessorKey: 'format', - header: 'Format', - }, - { - accessorKey: 'title', - header: 'Titre', - }, - { - accessorKey: 'creator', - header: 'Créateur', - }, - { - accessorKey: 'date', - header: 'Date publication', - }, - ], - data: [ - { - creator: 'Valentin Perello', - date: '20/05/2022', - format: 'SVG', - id: 1, - title: 'Doc test', - }, - { - creator: 'Valentin Perello', - date: '20/05/2022', - format: 'PDF', - id: 2, - title: 'Doc test', - }, - { - creator: 'Valentin Perello', - date: '20/05/2022', - format: 'PDF', - id: 3, - title: 'Doc test', - }, - { - creator: 'Valentin Perello', - date: '20/05/2022', - format: 'PDF', - id: 4, - title: 'Doc test', - }, - { - creator: 'Valentin Perello', - date: '20/05/2022', - format: 'PDF', - id: 5, - title: 'Doc test', - }, - ], - rowActionNumber: 3, + ...tableMock, }, }; diff --git a/packages/react-front-kit-table/src/Components/Table/Table.tsx b/packages/react-front-kit-table/src/Components/Table/Table.tsx index 6b5d3c0c..9b1605de 100644 --- a/packages/react-front-kit-table/src/Components/Table/Table.tsx +++ b/packages/react-front-kit-table/src/Components/Table/Table.tsx @@ -6,12 +6,10 @@ 'use client'; import type { FloatingPosition } from '@mantine/core/lib/Floating'; -import type { - IConfirmModalProps, - IPaginationProps, -} from '@smile/react-front-kit'; +import type { IPaginationProps } from '@smile/react-front-kit'; +import type { IAction, IConfirmAction } from '@smile/react-front-kit-shared'; import type { MRT_Row, MRT_TableOptions } from 'mantine-react-table'; -import type { ReactElement, ReactNode } from 'react'; +import type { ReactElement } from 'react'; import { ActionIcon, Button, Flex, Menu, Space, Tooltip } from '@mantine/core'; import { @@ -35,35 +33,17 @@ import { useState } from 'react'; import { useStyles } from './Table.style'; -export interface IActionConfirmModalProps> - extends Omit< - IConfirmModalProps, - 'onCancel' | 'onClose' | 'onConfirm' | 'opened' - > { - onCancel?: (row: MRT_Row | MRT_Row[]) => false | void; - onClose?: () => void; - onConfirm?: (row: MRT_Row | MRT_Row[]) => false | void; -} - -export interface IAction> { - color?: string; - confirmModalProps?: IActionConfirmModalProps; - confirmation?: boolean; - icon: ReactNode; - isMassAction?: boolean; - isRowAction?: boolean; - label: string; - onAction?: (row: MRT_Row | MRT_Row[]) => void; -} +type ITableAction> = IAction< + MRT_Row | MRT_Row[] +>; -export interface IConfirmAction> - extends IAction { - row: MRT_Row | MRT_Row[]; -} +type ITableConfirmAction> = IConfirmAction< + MRT_Row | MRT_Row[] +>; export interface ITableProps> extends MRT_TableOptions { - actions?: IAction[]; + actions?: ITableAction[]; menuLabel?: string; paginationProps?: Partial; rowActionNumber?: number; @@ -94,14 +74,14 @@ export function Table>( const { enablePagination = true, manualPagination } = mantineTableProps; const { classes } = useStyles(); const [confirmAction, setConfirmAction] = - useState | null>(null); + useState | null>(null); const [openedMenuRowIndex, setOpenedMenuRowIndex] = useState( null, ); // Calculated values const massActions = actions.filter(({ isMassAction }) => isMassAction); - const rowActions = actions.filter(({ isRowAction = true }) => isRowAction); + const rowActions = actions.filter(({ isItemAction = true }) => isItemAction); const visibleRowActions = rowActions.slice(0, rowActionNumber); const menuRowActions = rowActions.slice(rowActionNumber); @@ -115,19 +95,19 @@ export function Table>( } function handleAction( - row: MRT_Row | MRT_Row[], - action: IAction, + item: MRT_Row | MRT_Row[], + action: ITableAction, ): void { if (action.confirmation) { - setConfirmAction({ ...action, row }); + setConfirmAction({ ...action, item }); } else { - action.onAction?.(row); + action.onAction?.(item); } } function handleCancel(): void { if ( - confirmAction?.confirmModalProps?.onCancel?.(confirmAction.row) !== false + confirmAction?.confirmModalProps?.onCancel?.(confirmAction.item) !== false ) { setConfirmAction(null); } @@ -140,10 +120,11 @@ export function Table>( function handleConfirm(): void { if ( - confirmAction?.confirmModalProps?.onConfirm?.(confirmAction.row) !== false + confirmAction?.confirmModalProps?.onConfirm?.(confirmAction.item) !== + false ) { setConfirmAction(null); - confirmAction?.onAction?.(confirmAction.row); + confirmAction?.onAction?.(confirmAction.item); } } diff --git a/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.mock.tsx b/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.mock.tsx new file mode 100644 index 00000000..bbc15e0e --- /dev/null +++ b/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.mock.tsx @@ -0,0 +1,136 @@ +import type { ITableGridViewProps } from './TableGridView'; +import type { IThumbnail } from '@smile/react-front-kit'; +import type { IThumbnailAction } from '@smile/react-front-kit/src'; +import type { HandlerFunction } from '@storybook/addon-actions'; + +import { + DownloadSimple, + Eye, + PencilSimple, + ShareNetwork, + Star, + Trash, +} from '@phosphor-icons/react'; +import { FolderMove } from '@smile/react-front-kit'; +import { action } from '@storybook/addon-actions'; + +import { tableMock } from '../Table/Table.mock'; + +const thumbnailActionsMock: IThumbnailAction[] = [ + { + icon: , + id: 'move', + label: 'Move in tree', + onAction: action('Move in tree'), + }, + { + icon: , + id: 'open', + label: 'Open document', + onAction: action('Open document'), + }, + { + icon: , + id: 'edit', + label: 'Edit document', + onAction: action('Edit document'), + }, + { + icon: , + id: 'favorite', + label: 'Add to favorites', + onAction: action('Add to favorites'), + }, + { + icon: , + id: 'share', + label: 'Share', + onAction: action('Share'), + }, + { + icon: , + id: 'download', + label: 'Download', + onAction: action('Download'), + }, + { + color: 'red', + confirmModalProps: { + cancelLabel: 'Abort', + children:

Are you sure ?

, + confirmColor: 'red', + confirmLabel: 'Remove', + title: 'Remove File', + }, + confirmation: true, + icon: , + id: 'delete', + label: 'Delete', + onAction: action('Delete'), + }, +]; + +const baseThumbnailMock: Omit = { + actions: thumbnailActionsMock, + iconType: 'PDF', +}; + +const thumbnailsMock: IThumbnail[] = [ + { + id: '1', + label: 'Debit_Suivi_PREV', + ...baseThumbnailMock, + }, + { + id: '2', + label: 'Debit_Suivi_PREV_2', + ...baseThumbnailMock, + selected: true, + }, + { + id: '3', + label: 'Debit_Suivi_PREV_3', + ...baseThumbnailMock, + }, +]; + +const thumbnailGridMock = { + cols: 5, + gridActions: [ + { + icon: , + id: 'move', + 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 x files ?', + }, + confirmation: true, + icon: , + id: 'delete', + label: 'Delete', + onAction: action('Delete selected'), + }, + ], + onThumbnailClick: (): HandlerFunction => action('Thumbnail clicked'), + spacing: 25, + thumbnails: thumbnailsMock, + verticalSpacing: 25, +}; + +const { data, ...tableProps } = tableMock; +const { thumbnails, ...gridProps } = thumbnailGridMock; + +export const tableGridViewProps: ITableGridViewProps> = + { + data, + gridProps: { ...gridProps, idFieldName: 'id', labelFieldName: 'title' }, + tableProps, + }; diff --git a/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.stories.tsx b/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.stories.tsx new file mode 100644 index 00000000..4e678cd6 --- /dev/null +++ b/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { TableGridView as Cmp } from './TableGridView'; +import { tableGridViewProps } from './TableGridView.mock'; + +const meta = { + component: Cmp, + tags: ['autodocs'], + title: '3-custom/Components/TableGridView', +} satisfies Meta; + +export default meta; +type IStory = StoryObj; + +export const TableGridView: IStory = { + args: { ...tableGridViewProps }, +}; diff --git a/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.test.tsx b/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.test.tsx new file mode 100644 index 00000000..ec044ffa --- /dev/null +++ b/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.test.tsx @@ -0,0 +1,17 @@ +import { renderWithProviders } from '@smile/react-front-kit-shared/src/test-utils'; + +import { TableGridView } from './TableGridView'; +import { tableGridViewProps } from './TableGridView.mock'; + +describe('TableGridView', () => { + 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-table/src/Components/TableGridView/TableGridView.tsx b/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.tsx new file mode 100644 index 00000000..804dace7 --- /dev/null +++ b/packages/react-front-kit-table/src/Components/TableGridView/TableGridView.tsx @@ -0,0 +1,135 @@ +import type { ITableProps } from '../Table/Table'; +import type { + IDataView, + ISwitchableViewProps, + IThumbnail, + IThumbnailGridProps, +} from '@smile/react-front-kit'; +import type { MRT_RowSelectionState } from 'mantine-react-table'; +import type { ReactElement } from 'react'; + +import { createStyles } from '@mantine/styles'; +import { ListBullets, SquaresFour } from '@phosphor-icons/react'; +import { + SwitchableView, + ThumbnailGrid, + isNotNullNorEmpty, + typeGuard, +} from '@smile/react-front-kit'; +import { useState } from 'react'; + +import { Table } from '../Table/Table'; + +const useStyles = createStyles(() => ({ + tablePaper: { + border: 'none', + borderRadius: '4px', + }, +})); + +export interface ITableGridViewGridProps + extends Omit { + idFieldName: string; + imageFieldName?: string; + labelFieldName: string; +} + +export interface ITableGridViewProps> + extends Omit { + data: Data[]; + defaultView?: 'grid' | 'table'; + gridProps: ITableGridViewGridProps; + tableProps: Omit, 'data'>; +} + +export function TableGridView>( + props: ITableGridViewProps, +): ReactElement { + const { + data, + defaultView = 'table', + gridProps, + tableProps, + ...switchableViewProps + } = props; + const { idFieldName, labelFieldName, imageFieldName, ...otherGridProps } = + gridProps; + const [rowSelection, setRowSelection] = useState({}); + const selectedIndexes = Object.entries(rowSelection).map((entry) => + entry[1] ? entry[0] : null, + ); + const { classes } = useStyles(); + + const extendedTableProps: ITableProps = { + data, + enableBottomToolbar: false, + enableRowSelection: true, + mantinePaperProps: { + className: classes.tablePaper, + }, + onRowSelectionChange: setRowSelection, + state: { rowSelection }, + ...tableProps, + }; + + const thumbnails: IThumbnail[] = data + .map((item, index) => { + const id = item[gridProps.idFieldName]; + const label = item[gridProps.labelFieldName]; + const image = + gridProps.imageFieldName !== undefined + ? (item[gridProps.imageFieldName] as string) + : undefined; + const isSelected = selectedIndexes.includes(index.toString()); + + if ( + (typeGuard(id, 'number') || typeGuard(id, 'string')) && + typeGuard(label, 'string') + ) { + const thumbnail: IThumbnail = { + ...item, + id, + image, + label, + selected: isSelected, + }; + return thumbnail; + } + return null; + }) + .filter(isNotNullNorEmpty); + + function handleThumbnailSelect(index: number): void { + const newRowSelection = { ...rowSelection }; + newRowSelection[index] = !newRowSelection[index]; + setRowSelection(newRowSelection); + } + + const views: IDataView[] = [ + { + dataView: , + label: , + value: 'table', + }, + { + dataView: ( + handleThumbnailSelect(i)} + /> + ), + label: , + value: 'grid', + }, + ]; + + return ( + + ); +} diff --git a/packages/react-front-kit-table/src/Components/TableGridView/__snapshots__/TableGridView.test.tsx.snap b/packages/react-front-kit-table/src/Components/TableGridView/__snapshots__/TableGridView.test.tsx.snap new file mode 100644 index 00000000..32d5cb5b --- /dev/null +++ b/packages/react-front-kit-table/src/Components/TableGridView/__snapshots__/TableGridView.test.tsx.snap @@ -0,0 +1,2032 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TableGridView matches snapshot 1`] = ` +
+
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+
+
+
+ +
+
+
+ + +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+
+
+ + + + +
+
+
+
+ +
+ 1 + + SVG + + Doc test 1 + + Valentin Perello + + 16/05/2022 + +
+ + + + +
+
+ +
+
+
+ + + + +
+
+
+
+ +
+ 2 + + PDF + + Doc test 2 + + Valentin Perello + + 17/05/2022 + +
+ + + + +
+
+ +
+
+
+ + + + +
+
+
+
+ +
+ 3 + + PDF + + Doc test 3 + + Valentin Perello + + 18/05/2022 + +
+ + + + +
+
+ +
+
+
+ + + + +
+
+
+
+ +
+ 4 + + PDF + + Doc test 4 + + Valentin Perello + + 19/05/2022 + +
+ + + + +
+
+ +
+
+
+ + + + +
+
+
+
+ +
+ 5 + + PDF + + Doc test 5 + + Valentin Perello + + 20/05/2022 + +
+ + + + +
+
+ +
+
+
+ + + + +
+
+
+
+ +
+ 6 + + PDF + + Doc test 6 + + Valentin Perello + + 21/05/2022 + +
+ + + + +
+
+ +
+
+
+ + + + +
+
+
+
+ +
+ 7 + + PDF + + Doc test 7 + + Valentin Perello + + 22/05/2022 + +
+ + + + +
+
+ + +
+
+
+ +
+
+
+ + +`; diff --git a/packages/react-front-kit/src/Components/ConfirmModal/ConfirmModal.tsx b/packages/react-front-kit/src/Components/ConfirmModal/ConfirmModal.tsx index ff2e1042..5b8e710a 100644 --- a/packages/react-front-kit/src/Components/ConfirmModal/ConfirmModal.tsx +++ b/packages/react-front-kit/src/Components/ConfirmModal/ConfirmModal.tsx @@ -1,21 +1,13 @@ 'use client'; -import type { MantineColor, ModalProps } from '@mantine/core'; +import type { IConfirmModal } from '@smile/react-front-kit-shared/src/types/actions'; import type { ReactElement } from 'react'; import { Button, Modal } from '@mantine/core'; import { useStyles } from './ConfirmModal.style'; -export interface IConfirmModalProps extends ModalProps { - cancelColor?: MantineColor; - cancelLabel?: string; - confirmColor?: MantineColor; - confirmLabel?: string; - onCancel?: () => void; - onConfirm?: () => void; - title?: string; -} +type IConfirmModalProps = IConfirmModal; export function ConfirmModal(props: IConfirmModalProps): ReactElement { const { diff --git a/packages/react-front-kit/src/Components/SwitchableView/SwitchableView.tsx b/packages/react-front-kit/src/Components/SwitchableView/SwitchableView.tsx index f6ffd2e7..2afa1287 100644 --- a/packages/react-front-kit/src/Components/SwitchableView/SwitchableView.tsx +++ b/packages/react-front-kit/src/Components/SwitchableView/SwitchableView.tsx @@ -45,7 +45,7 @@ export interface IDataView extends SegmentedControlItem { dataView: ReactNode; } -interface ISwitchableViewProps extends PaperProps { +export interface ISwitchableViewProps extends PaperProps { /** Default index of the active view */ defaultValue?: number; /** Called when active view changes */ diff --git a/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.mock.tsx b/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.mock.tsx new file mode 100644 index 00000000..285b153e --- /dev/null +++ b/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.mock.tsx @@ -0,0 +1,66 @@ +import type { IThumbnailAction } from './Thumbnail'; + +import { + DownloadSimple, + Eye, + PencilSimple, + ShareNetwork, + Star, + Trash, +} from '@phosphor-icons/react'; +import { FolderMove } from '@smile/react-front-kit-shared'; +import { action } from '@storybook/addon-actions'; + +export const thumbnailActions: IThumbnailAction[] = [ + { + icon: , + id: 'move', + label: 'Move in tree', + onAction: action('Move in tree'), + }, + { + icon: , + id: 'open', + label: 'Open document', + onAction: action('Open document'), + }, + { + icon: , + id: 'edit', + label: 'Edit document', + onAction: action('Edit document'), + }, + { + icon: , + id: 'favorite', + label: 'Add to favorites', + onAction: action('Add to favorites'), + }, + { + icon: , + id: 'share', + label: 'Share', + onAction: action('Share'), + }, + { + icon: , + id: 'download', + label: 'Download', + onAction: action('Download'), + }, + { + color: 'red', + confirmModalProps: { + cancelLabel: 'Abort', + children:

Are you sure ?

, + confirmColor: 'red', + confirmLabel: 'Remove', + title: 'Remove File', + }, + confirmation: true, + icon: , + id: 'delete', + label: 'Delete', + onAction: action('Delete'), + }, +]; diff --git a/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.stories.tsx b/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.stories.tsx index 6b259858..896445c1 100644 --- a/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.stories.tsx +++ b/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.stories.tsx @@ -1,17 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { - DownloadSimple, - Eye, - PencilSimple, - ShareNetwork, - Star, - Trash, -} from '@phosphor-icons/react'; -import { FolderMove } from '@smile/react-front-kit-shared'; -import { action } from '@storybook/addon-actions'; - import { Thumbnail as Cmp } from './Thumbnail'; +import { thumbnailActions } from './Thumbnail.mock'; const meta = { component: Cmp, @@ -24,53 +14,9 @@ type IStory = StoryObj; export const Thumbnail: IStory = { args: { - action: [ - { - icon: , - label: 'Move in tree', - onAction: action('Move in tree'), - }, - { - icon: , - label: 'Open document', - onAction: action('Open document'), - }, - { - icon: , - label: 'Edit document', - onAction: action('Edit document'), - }, - { - icon: , - label: 'Add to favorites', - onAction: action('Add to favorites'), - }, - { - icon: , - label: 'Share', - onAction: action('Share'), - }, - { - icon: , - label: 'Download', - onAction: action('Download'), - }, - { - color: 'red', - confirmModalProps: { - cancelLabel: 'Abord', - children:

Are you sur ?

, - confirmColor: 'red', - confirmLabel: 'Remove', - title: 'Remove File', - }, - confirmation: true, - icon: , - label: 'Delete', - onAction: action('Delete'), - }, - ], + actions: thumbnailActions, iconType: 'PDF', + id: '1', label: 'Debit_Suivi_PREV', selected: false, }, diff --git a/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.style.tsx b/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.style.tsx index 80b3190a..d47f02a7 100644 --- a/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.style.tsx +++ b/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.style.tsx @@ -50,7 +50,8 @@ export const useStyles = createStyles((theme) => ({ root: { background: theme.colors.gray[1], borderRadius: '16px', - heigh: 'auto', + cursor: 'pointer', + height: 'auto', padding: '16px', width: 'auto', }, diff --git a/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.test.tsx b/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.test.tsx index f8d32278..a16ebb00 100644 --- a/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.test.tsx +++ b/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.test.tsx @@ -4,7 +4,7 @@ import { Thumbnail } from './Thumbnail'; describe('Thumbnail', () => { it('matches snapshot', () => { - const { container } = renderWithProviders(); + const { container } = renderWithProviders(); expect(container).toMatchSnapshot(); }); }); diff --git a/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.tsx b/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.tsx index cf7c4a1f..c107ca38 100644 --- a/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.tsx +++ b/packages/react-front-kit/src/Components/Thumbnail/Thumbnail.tsx @@ -1,7 +1,10 @@ 'use client'; -import type { IConfirmModalProps } from '../ConfirmModal/ConfirmModal'; -import type { ReactElement, ReactNode } from 'react'; +import type { + IAction, + IActionConfirmModalProps, +} from '@smile/react-front-kit-shared/src/types/actions'; +import type { ReactElement } from 'react'; import { ActionIcon, @@ -21,34 +24,26 @@ import { ConfirmModal } from '../ConfirmModal/ConfirmModal'; import { useStyles } from './Thumbnail.style'; -export type IActionConfirmModalProps = Omit< - IConfirmModalProps, - 'onClose' | 'opened' ->; - -export interface IThumbnailAction { - color?: string; - confirmModalProps?: IActionConfirmModalProps; - confirmation?: boolean; - icon: ReactNode; - label: string; - onAction: () => void; -} - -export interface IThumbnailProps { - action?: IThumbnailAction[]; +export interface IThumbnail extends Record { iconType?: string; + id: number | string; image?: string; label?: string; onClick?: () => void; selected?: boolean; } +export type IThumbnailAction = IAction; + +export interface IThumbnailProps extends IThumbnail { + actions?: IThumbnailAction[]; +} + export function Thumbnail(props: IThumbnailProps): ReactElement { const { classes } = useStyles(); const theme = useMantineTheme(); const { - action = [], + actions = [], iconType, image = defaultImage, label, @@ -57,9 +52,9 @@ export function Thumbnail(props: IThumbnailProps): ReactElement { } = props; const [confirmAction, setConfirmAction] = - useState(null); + useState | null>(null); - function clearconfirmAction(): void { + function clearConfirmAction(): void { setConfirmAction(null); } @@ -75,7 +70,7 @@ export function Thumbnail(props: IThumbnailProps): ReactElement { children: action.confirmModalProps?.children, confirmColor: action.confirmModalProps?.confirmColor, confirmLabel: action.confirmModalProps?.confirmLabel, - onConfirm: action.onAction, + onConfirm: () => action.onAction?.(props), title: action.confirmModalProps?.title, }); } @@ -84,15 +79,16 @@ export function Thumbnail(props: IThumbnailProps): ReactElement { if (action.confirmation) { setModal(action); } else { - action.onAction(); + action.onAction?.(props); } } function handleClose(): void { - clearconfirmAction(); + clearConfirmAction(); } - function handleModalButton(onAction?: () => void): void { - onAction && onAction(); + + function handleModalButton(onAction?: (item: IThumbnail) => void): void { + onAction && onAction(props); handleClose(); } @@ -118,13 +114,14 @@ export function Thumbnail(props: IThumbnailProps): ReactElement {
- {action.length > 0 && ( + {actions.length > 0 && ( e.stopPropagation()} radius={4} type="button" > @@ -137,8 +134,8 @@ export function Thumbnail(props: IThumbnailProps): ReactElement {
- - {action.map((action, index) => ( + e.stopPropagation()}> + {actions.map((action, index) => (
= { + actions: thumbnailActions, + iconType: 'PDF', +}; + +export const thumbnails: IThumbnail[] = [ + { + id: '1', + label: 'Debit_Suivi_PREV', + ...baseThumbnail, + }, + { + id: '2', + label: 'Debit_Suivi_PREV_2', + ...baseThumbnail, + selected: true, + }, + { + id: '3', + label: 'Debit_Suivi_PREV_3', + ...baseThumbnail, + }, +]; + +export const thumbnailGridMock = { + cols: 5, + gridActions: [ + { + icon: , + id: 'move', + 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 x files ?', + }, + confirmation: true, + icon: , + id: 'delete', + label: 'Delete', + onAction: action('Delete selected'), + }, + ], + onThumbnailClick: (): HandlerFunction => action('Thumbnail clicked'), + spacing: 25, + thumbnails, + verticalSpacing: 25, +}; diff --git a/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.stories.tsx b/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.stories.tsx new file mode 100644 index 00000000..93eb6303 --- /dev/null +++ b/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ThumbnailGrid as Cmp } from './ThumbnailGrid'; +import { thumbnailGridMock } from './ThumbnailGrid.mock'; + +const meta = { + component: Cmp, + tags: ['autodocs'], + title: '3-custom/Components/ThumbnailGrid', +} satisfies Meta; + +export default meta; +type IStory = StoryObj; + +export const ThumbnailGrid: IStory = { + args: { ...thumbnailGridMock }, +}; diff --git a/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.test.tsx b/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.test.tsx new file mode 100644 index 00000000..c3c9c385 --- /dev/null +++ b/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.test.tsx @@ -0,0 +1,17 @@ +import { renderWithProviders } from '@smile/react-front-kit-shared/src/test-utils'; + +import { ThumbnailGrid } from './ThumbnailGrid'; +import { thumbnails } from './ThumbnailGrid.mock'; + +describe('ThumbnailGrid', () => { + 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/ThumbnailGrid/ThumbnailGrid.tsx b/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.tsx new file mode 100644 index 00000000..81339ddf --- /dev/null +++ b/packages/react-front-kit/src/Components/ThumbnailGrid/ThumbnailGrid.tsx @@ -0,0 +1,147 @@ +'use client'; + +import type { IThumbnail } from '../Thumbnail/Thumbnail'; +import type { SimpleGridProps } from '@mantine/core'; +import type { + IAction, + IActionConfirmModalProps, +} from '@smile/react-front-kit-shared'; +import type { ReactElement } from 'react'; + +import { Button, Group, SimpleGrid } from '@mantine/core'; +import { createStyles } from '@mantine/styles'; +import { useState } from 'react'; + +import { ConfirmModal } from '../ConfirmModal/ConfirmModal'; +import { Thumbnail } from '../Thumbnail/Thumbnail'; + +const useStyles = createStyles((theme) => ({ + 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 IGridAction = IAction; +type IGridActionConfirmModalProps = IActionConfirmModalProps; + +export interface IThumbnailGridProps extends SimpleGridProps { + gridActions?: IGridAction[]; + onThumbnailClick?: (item: IThumbnail, index: number) => void; + selectedElementsText?: (numberOfSelectedElements: number) => string; + thumbnails: IThumbnail[]; +} + +/** Additional props will be forwarded to the [Mantine SimpleGrid component](https://mantine.dev/core/simple-grid) */ +export function ThumbnailGrid(props: IThumbnailGridProps): ReactElement { + const { + gridActions = [], + onThumbnailClick, + selectedElementsText = defaultSelectedElementsText, + thumbnails, + ...simpleGridProps + } = props; + + const selectedElements = thumbnails.filter((thumbnail) => thumbnail.selected); + const numberOfSelectedElements = selectedElements.length; + const [confirmAction, setConfirmAction] = + useState(null); + + const { classes } = useStyles(); + + function handleSelect(index: number): void { + onThumbnailClick?.(thumbnails[index], index); + } + + function setModal(action: IGridAction): 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: IGridAction): 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)} + {gridActions.length > 0 && ( + + {gridActions.map((action) => ( + + ))} + + )} +
+ )} + + {thumbnails.map((thumbnail, index) => ( + handleSelect(index)} + {...thumbnail} + /> + ))} + +
+ handleModalButton(confirmAction?.onCancel)} + onClose={handleClose} + onConfirm={() => handleModalButton(confirmAction?.onConfirm)} + opened={Boolean(confirmAction)} + > + {confirmAction?.children} + + + ); +} 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 new file mode 100644 index 00000000..2a9541e2 --- /dev/null +++ b/packages/react-front-kit/src/Components/ThumbnailGrid/__snapshots__/ThumbnailGrid.test.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ThumbnailGrid matches snapshot 1`] = ` +
+
+
+ + 1 selected file + +
+
+
+
+
+ + + +

+ Debit_Suivi_PREV +

+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +

+ Debit_Suivi_PREV_2 +

+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +

+ Debit_Suivi_PREV_3 +

+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+`; diff --git a/packages/react-front-kit/src/helpers/index.ts b/packages/react-front-kit/src/helpers/index.ts index 53af472e..a22f1944 100644 --- a/packages/react-front-kit/src/helpers/index.ts +++ b/packages/react-front-kit/src/helpers/index.ts @@ -1 +1,2 @@ export * from './nestedObject'; +export * from './typeGuard'; diff --git a/packages/react-front-kit/src/helpers/typeGuard.ts b/packages/react-front-kit/src/helpers/typeGuard.ts new file mode 100644 index 00000000..25b432a5 --- /dev/null +++ b/packages/react-front-kit/src/helpers/typeGuard.ts @@ -0,0 +1,28 @@ +export interface ITypeMap { + boolean: boolean; + number: number; + string: string; +} + +export type IPrimitiveOrConstructor = // 'string' | 'number' | 'boolean' | constructor + keyof ITypeMap | (new (...args: unknown[]) => unknown); + +// infer the guarded type from a specific case of PrimitiveOrConstructor +export type IGuardedType = T extends new ( + ...args: unknown[] +) => infer U + ? U + : T extends keyof ITypeMap + ? ITypeMap[T] + : never; + +export function typeGuard( + o: unknown, + className: T, +): o is IGuardedType { + const localPrimitiveOrConstructor: IPrimitiveOrConstructor = className; + if (typeof localPrimitiveOrConstructor === 'string') { + return typeof o === localPrimitiveOrConstructor; + } + return o instanceof localPrimitiveOrConstructor; +} diff --git a/packages/react-front-kit/src/index.tsx b/packages/react-front-kit/src/index.tsx index 9818e859..a20faa6c 100644 --- a/packages/react-front-kit/src/index.tsx +++ b/packages/react-front-kit/src/index.tsx @@ -13,7 +13,9 @@ export * from './Components/Header/Header'; export * from './Components/HeaderSearch/HeaderSearch'; export * from './Components/Pagination/Pagination'; export * from './Components/SidebarMenu/SidebarMenu'; +export * from './Components/SwitchableView/SwitchableView'; export * from './Components/Thumbnail/Thumbnail'; +export * from './Components/ThumbnailGrid/ThumbnailGrid'; // layout exports export * from './Layouts/FoldableColumnLayout/FoldableColumnLayout'; // page exports