diff --git a/package-lock.json b/package-lock.json index cba706a1..e9a2fa1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26488,9 +26488,9 @@ } }, "node_modules/telejson": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.1.0.tgz", - "integrity": "sha512-jFJO4P5gPebZAERPkJsqMAQ0IMA1Hi0AoSfxpnUaV6j6R2SZqlpkbS20U6dEUtA3RUYt2Ak/mTlkQzHH9Rv/hA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.2.0.tgz", + "integrity": "sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==", "dev": true, "dependencies": { "memoizerific": "^1.11.3" @@ -28227,6 +28227,7 @@ "@storybook/addon-links": "^7.1.0", "@storybook/blocks": "^7.1.0", "@storybook/jest": "^0.1.0", + "@storybook/preview-api": "^7.4.0", "@storybook/react": "^7.1.0", "@storybook/react-vite": "^7.1.0", "@storybook/test-runner": "^0.11.0", @@ -28248,6 +28249,104 @@ "wait-on": "^7.0.1" } }, + "packages/react-front-kit/node_modules/@storybook/channels": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.4.0.tgz", + "integrity": "sha512-/1CU0s3npFumzVHLGeubSyPs21O3jNqtSppOjSB9iDTyV2GtQrjh5ntVwebfKpCkUSitx3x7TkCb9dylpEZ8+w==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "7.4.0", + "@storybook/core-events": "7.4.0", + "@storybook/global": "^5.0.0", + "qs": "^6.10.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "packages/react-front-kit/node_modules/@storybook/client-logger": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.4.0.tgz", + "integrity": "sha512-4pBnf7+df1wXEVcF1civqxbrtccGGHQkfWQkJo49s53RXvF7SRTcif6XTx0V3cQV0v7I1C5mmLm0LNlmjPRP1Q==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "packages/react-front-kit/node_modules/@storybook/core-events": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.4.0.tgz", + "integrity": "sha512-JavEo4dw7TQdF5pSKjk4RtqLgsG2R/eWRI8vZ3ANKa0ploGAnQR/eMTfSxf6TUH3ElBWLJhi+lvUCkKXPQD+dw==", + "dev": true, + "dependencies": { + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "packages/react-front-kit/node_modules/@storybook/preview-api": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.4.0.tgz", + "integrity": "sha512-ndXO0Nx+eE7ktVE4EqHpQZ0guX7yYBdruDdJ7B739C0+OoPWsJN7jAzUqq0NXaBcYrdaU5gTy+KnWJUt8R+OyA==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.4.0", + "@storybook/client-logger": "7.4.0", + "@storybook/core-events": "7.4.0", + "@storybook/csf": "^0.1.0", + "@storybook/global": "^5.0.0", + "@storybook/types": "7.4.0", + "@types/qs": "^6.9.5", + "dequal": "^2.0.2", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "synchronous-promise": "^2.0.15", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "packages/react-front-kit/node_modules/@storybook/types": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.0.tgz", + "integrity": "sha512-XyzYkmeklywxvElPrIWLczi/PWtEdgTL6ToT3++FVxptsC2LZKS3Ue+sBcQ9xRZhkRemw4HQHwed5EW3dO8yUg==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.4.0", + "@types/babel__core": "^7.0.0", + "@types/express": "^4.7.0", + "@types/react": "^16.14.34", + "file-system-cache": "2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "packages/react-front-kit/node_modules/@storybook/types/node_modules/@types/react": { + "version": "16.14.46", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.46.tgz", + "integrity": "sha512-Am4pyXMrr6cWWw/TN3oqHtEZl0j+G6Up/O8m65+xF/3ZaUgkv1GAtTPWw4yNRmH0HJXmur6xKCKoMo3rBGynuw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, "packages/react-front-kit/node_modules/axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -46036,6 +46135,7 @@ "@storybook/addon-links": "^7.1.0", "@storybook/blocks": "^7.1.0", "@storybook/jest": "^0.1.0", + "@storybook/preview-api": "^7.4.0", "@storybook/react": "^7.1.0", "@storybook/react-vite": "^7.1.0", "@storybook/test-runner": "^0.11.0", @@ -46059,6 +46159,86 @@ "wait-on": "^7.0.1" }, "dependencies": { + "@storybook/channels": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.4.0.tgz", + "integrity": "sha512-/1CU0s3npFumzVHLGeubSyPs21O3jNqtSppOjSB9iDTyV2GtQrjh5ntVwebfKpCkUSitx3x7TkCb9dylpEZ8+w==", + "dev": true, + "requires": { + "@storybook/client-logger": "7.4.0", + "@storybook/core-events": "7.4.0", + "@storybook/global": "^5.0.0", + "qs": "^6.10.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" + } + }, + "@storybook/client-logger": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.4.0.tgz", + "integrity": "sha512-4pBnf7+df1wXEVcF1civqxbrtccGGHQkfWQkJo49s53RXvF7SRTcif6XTx0V3cQV0v7I1C5mmLm0LNlmjPRP1Q==", + "dev": true, + "requires": { + "@storybook/global": "^5.0.0" + } + }, + "@storybook/core-events": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.4.0.tgz", + "integrity": "sha512-JavEo4dw7TQdF5pSKjk4RtqLgsG2R/eWRI8vZ3ANKa0ploGAnQR/eMTfSxf6TUH3ElBWLJhi+lvUCkKXPQD+dw==", + "dev": true, + "requires": { + "ts-dedent": "^2.0.0" + } + }, + "@storybook/preview-api": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.4.0.tgz", + "integrity": "sha512-ndXO0Nx+eE7ktVE4EqHpQZ0guX7yYBdruDdJ7B739C0+OoPWsJN7jAzUqq0NXaBcYrdaU5gTy+KnWJUt8R+OyA==", + "dev": true, + "requires": { + "@storybook/channels": "7.4.0", + "@storybook/client-logger": "7.4.0", + "@storybook/core-events": "7.4.0", + "@storybook/csf": "^0.1.0", + "@storybook/global": "^5.0.0", + "@storybook/types": "7.4.0", + "@types/qs": "^6.9.5", + "dequal": "^2.0.2", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "synchronous-promise": "^2.0.15", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/types": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.0.tgz", + "integrity": "sha512-XyzYkmeklywxvElPrIWLczi/PWtEdgTL6ToT3++FVxptsC2LZKS3Ue+sBcQ9xRZhkRemw4HQHwed5EW3dO8yUg==", + "dev": true, + "requires": { + "@storybook/channels": "7.4.0", + "@types/babel__core": "^7.0.0", + "@types/express": "^4.7.0", + "@types/react": "^16.14.34", + "file-system-cache": "2.3.0" + }, + "dependencies": { + "@types/react": { + "version": "16.14.46", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.46.tgz", + "integrity": "sha512-Am4pyXMrr6cWWw/TN3oqHtEZl0j+G6Up/O8m65+xF/3ZaUgkv1GAtTPWw4yNRmH0HJXmur6xKCKoMo3rBGynuw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + } + } + }, "axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -47511,9 +47691,9 @@ } }, "telejson": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.1.0.tgz", - "integrity": "sha512-jFJO4P5gPebZAERPkJsqMAQ0IMA1Hi0AoSfxpnUaV6j6R2SZqlpkbS20U6dEUtA3RUYt2Ak/mTlkQzHH9Rv/hA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.2.0.tgz", + "integrity": "sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==", "dev": true, "requires": { "memoizerific": "^1.11.3" diff --git a/packages/react-front-kit/package.json b/packages/react-front-kit/package.json index 3c86dcc0..7b98e0c7 100644 --- a/packages/react-front-kit/package.json +++ b/packages/react-front-kit/package.json @@ -63,6 +63,7 @@ "@storybook/addon-links": "^7.1.0", "@storybook/blocks": "^7.1.0", "@storybook/jest": "^0.1.0", + "@storybook/preview-api": "^7.4.0", "@storybook/react": "^7.1.0", "@storybook/react-vite": "^7.1.0", "@storybook/test-runner": "^0.11.0", diff --git a/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.stories.tsx b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.stories.tsx new file mode 100644 index 00000000..bfc09253 --- /dev/null +++ b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { useStorybookArgsConnect } from '../../../hooks/useStorybookArgsConnect'; + +import { Pagination as Cmp } from './Pagination'; + +const meta = { + component: Cmp, + decorators: [ + function Component(Story, ctx) { + const args = useStorybookArgsConnect(ctx.args, { + onPageChange: 'page', + onRowsPerPageChange: 'rowsPerPage', + }); + return ; + }, + ], + tags: ['autodocs'], + title: '3-custom/Components/Pagination', +} satisfies Meta; + +export default meta; +type IStory = StoryObj; + +export const Pagination: IStory = { + args: { + page: 2, + rowsPerPage: 15, + rowsPerPageLabel: 'Number of results per page', + rowsPerPageOptions: [ + { label: 'Display 1 result', value: 1 }, + { label: 'Display 5 results', value: 5 }, + { label: 'Display 10 results', value: 10 }, + { label: 'Display 15 results', value: 15 }, + ], + totalPages: 10, + }, +}; diff --git a/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.test.tsx b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.test.tsx new file mode 100644 index 00000000..087fbd70 --- /dev/null +++ b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.test.tsx @@ -0,0 +1,62 @@ +import { expect } from '@storybook/jest'; +import { within } from '@storybook/testing-library'; + +import { renderWithProviders } from '../../../utils/tests'; + +import { Pagination } from './Pagination'; + +describe('Pagination', () => { + beforeEach(() => { + // Prevent mantine random ID + Math.random = () => 0.42; + }); + + it('matches snapshot', () => { + const { container } = renderWithProviders( + + ); + expect(container).toMatchSnapshot(); + }); + + it('renders with minimal props', () => { + const { container } = renderWithProviders( + + ); + const canvas = within(container); + expect(canvas.queryByTestId('pagination')).toBeVisible(); + expect(canvas.queryByTestId('pagination-rowsPerPage')).toBeNull(); + expect(canvas.queryByTestId('pagination-page')).toBeVisible(); + }); + + it('renders with full props', () => { + const { container } = renderWithProviders( + + ); + const canvas = within(container); + expect(canvas.queryByTestId('pagination')).toBeVisible(); + expect(canvas.queryByTestId('pagination-rowsPerPage')).toBeVisible(); + expect(canvas.queryByTestId('pagination-page')).toBeVisible(); + }); +}); diff --git a/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.tsx b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.tsx new file mode 100644 index 00000000..26e90d03 --- /dev/null +++ b/packages/react-front-kit/src/3-custom/Components/Pagination/Pagination.tsx @@ -0,0 +1,93 @@ +'use client'; +import type { PaginationProps } from '@mantine/core'; +import type { FlexProps } from '@mantine/core/lib/Flex/Flex'; +import type { SelectProps } from '@mantine/core/lib/Select/Select'; +import type { ReactElement } from 'react'; + +import { + Flex, + Pagination as MantinePagination, + Select, + createStyles, +} from '@mantine/core'; + +const useStyles = createStyles(() => ({ + container: { + justifyContent: 'space-between', + }, +})); + +export interface IRowsPerPageOption { + label?: string; + value: number; +} + +interface IPaginationProps extends FlexProps { + onPageChange?: (value: number) => void; + onRowsPerPageChange?: (value: number) => void; + page: number; + paginationProps?: PaginationProps; + rowsPerPage: number; + rowsPerPageLabel?: string; + rowsPerPageOptions?: IRowsPerPageOption[]; + selectProps?: SelectProps; + totalPages: number; +} + +export function Pagination(props: IPaginationProps): ReactElement { + const { + onPageChange, + onRowsPerPageChange, + page, + paginationProps, + rowsPerPage, + rowsPerPageLabel, + rowsPerPageOptions = [], + selectProps, + totalPages, + ...flexProps + } = props; + const { classes } = useStyles(); + + /* methods */ + function handleChangeRowsPerPage(value: string): void { + if (value) { + onRowsPerPageChange?.(Number(value)); + } + } + + function handleChangePage(value: number): void { + onPageChange?.(value); + } + + return ( + + {rowsPerPageOptions.length > 0 && rowsPerPageLabel ? ( + +
+ +
+ + + +
+
+ + +
+ + + + + +
+ + + +
+ +
+ + +`; diff --git a/packages/react-front-kit/src/hooks/useStorybookArgsConnect.ts b/packages/react-front-kit/src/hooks/useStorybookArgsConnect.ts new file mode 100644 index 00000000..5f822a74 --- /dev/null +++ b/packages/react-front-kit/src/hooks/useStorybookArgsConnect.ts @@ -0,0 +1,23 @@ +import { useArgs } from '@storybook/preview-api'; + +import { isCallback } from '../utils/utilities'; + +export function useStorybookArgsConnect>( + args: T, + propsToConnect: Record +): T { + const [, setArgs] = useArgs(); + const connectedProps = Object.entries(propsToConnect) + .filter(([action]) => isCallback(args[action])) + .map(([action, prop]) => [ + [action], + (...params: unknown[]): void => { + (args[action] as (...params: unknown[]) => void)?.(...params); + if (args[prop] !== undefined) { + // We suppose the value is passed as the first argument in the callback + setArgs({ [prop]: params[0] } as Partial); + } + }, + ]); + return { ...args, ...Object.fromEntries(connectedProps) }; +} diff --git a/packages/react-front-kit/src/index.tsx b/packages/react-front-kit/src/index.tsx index cdebd8cf..ce96b15c 100644 --- a/packages/react-front-kit/src/index.tsx +++ b/packages/react-front-kit/src/index.tsx @@ -4,6 +4,7 @@ export * from './3-custom/Components/SidebarMenu/SidebarMenu'; export * from './3-custom/Components/HeaderSearch/HeaderSearch'; export * from './3-custom/Components/DropdownButton/DropdownButton'; export * from './3-custom/Components/Header/Header'; +export * from './3-custom/Components/Pagination/Pagination'; export * from './3-custom/Pages/TestPage/TestPage'; export * from './3-custom/Provider/Provider'; export * from './theme'; diff --git a/packages/react-front-kit/src/utils/utilities.ts b/packages/react-front-kit/src/utils/utilities.ts new file mode 100644 index 00000000..53edc747 --- /dev/null +++ b/packages/react-front-kit/src/utils/utilities.ts @@ -0,0 +1,9 @@ +export function isNotNullNotEmpty( + value: S | undefined | Record +): value is Exclude { + return value != null && Object.keys(value).length !== 0; +} + +export function isCallback(maybeFunc: T | unknown): maybeFunc is T { + return typeof maybeFunc === 'function'; +}