diff --git a/.storybook/main.ts b/.storybook/main.ts index 3baab991ce..5d9abfe918 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,6 +1,9 @@ import type { StorybookConfig } from '@storybook/react-webpack5'; const config: StorybookConfig = { - stories: ['../stories/**/*.@(mdx|stories.@(ts|tsx))'], + stories: [ + '../stories/**/*.@(mdx|stories.@(ts|tsx))', + '../stories/***/**/*.@(mdx|stories.@(ts|tsx))', + ], staticDirs: ['./public'], addons: [ '@storybook/addon-essentials', @@ -8,7 +11,7 @@ const config: StorybookConfig = { '@storybook/addon-mdx-gfm', ], - webpackFinal: async (config, { configType }) => { + webpackFinal: async (config) => { // Resolve error when webpack-ing storybook: // Can't import the named export 'Children' from non EcmaScript module (only // default export is available) diff --git a/.storybook/preview.js b/.storybook/preview.js index 29e8227b00..73a94eaec2 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -3,7 +3,7 @@ import { QueryClient, QueryClientProvider } from 'react-query'; import { CoreUiThemeProvider } from '../src/lib/next'; import { brand, coreUIAvailableThemes } from '../src/lib/style/theme'; import { Wrapper } from '../stories/common'; -import { ScrollbarWrapper, ToastProvider } from '../src/lib'; +import { ToastProvider } from '../src/lib'; export const globalTypes = { theme: { diff --git a/src/lib/components/infomessage/InfoMessage.component.tsx b/src/lib/components/infomessage/InfoMessage.component.tsx index c29903dde8..a5a6d4a606 100644 --- a/src/lib/components/infomessage/InfoMessage.component.tsx +++ b/src/lib/components/infomessage/InfoMessage.component.tsx @@ -39,7 +39,7 @@ export const InfoMessage = ({ title, content, link }: Props) => { {link && ( - More infos + More info )} diff --git a/src/lib/components/tablev2/MultiSelectableContent.tsx b/src/lib/components/tablev2/MultiSelectableContent.tsx index 457c17ef1e..a9ddf79481 100644 --- a/src/lib/components/tablev2/MultiSelectableContent.tsx +++ b/src/lib/components/tablev2/MultiSelectableContent.tsx @@ -1,7 +1,15 @@ -import { useEffect, memo, CSSProperties } from 'react'; +import { memo, useEffect } from 'react'; import { Row } from 'react-table'; import { areEqual } from 'react-window'; -import { useTableContext } from './Tablev2.component'; +import { spacing } from '../../spacing'; +import { Box } from '../box/Box'; +import { Loader } from '../loader/Loader.component'; +import { RenderRowType, TableRows, useTableScrollbar } from './TableCommon'; +import { + TableHeightKeyType, + TableLocalType, + TableVariantType, +} from './TableUtils'; import { HeadRow, SortCaret, @@ -9,16 +17,8 @@ import { TableHeader, TableRowMultiSelectable, } from './Tablestyle'; -import { - TableHeightKeyType, - TableLocalType, - TableVariantType, -} from './TableUtils'; -import { RenderRowType, TableRows, useTableScrollbar } from './TableCommon'; +import { useTableContext } from './Tablev2.component'; import useSyncedScroll from './useSyncedScroll'; -import { Box } from '../box/Box'; -import { Loader } from '../loader/Loader.component'; -import { spacing } from '../../spacing'; type MultiSelectableContentProps< DATA_ROW extends Record = Record, @@ -204,6 +204,7 @@ export const MultiSelectableContent = < rowHeight={rowHeight} separationLineVariant={separationLineVariant} ref={headerRef} + style={{}} > {headerGroup.headers.map((column) => { const headerStyleProps = column.getHeaderProps( diff --git a/src/lib/components/tablev2/SingleSelectableContent.tsx b/src/lib/components/tablev2/SingleSelectableContent.tsx index 016773467d..4df8a10f39 100644 --- a/src/lib/components/tablev2/SingleSelectableContent.tsx +++ b/src/lib/components/tablev2/SingleSelectableContent.tsx @@ -138,7 +138,7 @@ export function SingleSelectableContent< hasScrollBar={hasScrollbar} scrollBarWidth={scrollBarWidth} rowHeight={rowHeight} - style={{ overflow: 'hidden' }} + style={{}} > {headerGroup.headers.map((column) => { const headerStyleProps = column.getHeaderProps( diff --git a/src/lib/components/tablev2/Tablestyle.tsx b/src/lib/components/tablev2/Tablestyle.tsx index 4bed1d618d..b818857b6c 100644 --- a/src/lib/components/tablev2/Tablestyle.tsx +++ b/src/lib/components/tablev2/Tablestyle.tsx @@ -117,6 +117,7 @@ type TableRowMultiSelectableType = { }; export const TableRowMultiSelectable = styled.div` color: ${(props) => props.theme.textPrimary}; + gap: ${spacing.r16}; border-bottom: 1px solid ${(props) => props.theme[props.separationLineVariant]}; box-sizing: border-box; diff --git a/stories/InlineInput/InlineInput.stories.tsx b/stories/InlineInput/InlineInput.stories.tsx index ff3afc3a1b..1fd512cf1d 100644 --- a/stories/InlineInput/InlineInput.stories.tsx +++ b/stories/InlineInput/InlineInput.stories.tsx @@ -26,7 +26,9 @@ export default { }); }, }); - return ; + return ( + + ); }, }; diff --git a/stories/Table/tablev2.guideline.mdx b/stories/Table/tablev2.guideline.mdx new file mode 100644 index 0000000000..3ca66fc7c6 --- /dev/null +++ b/stories/Table/tablev2.guideline.mdx @@ -0,0 +1,90 @@ +import { + Meta, + Story, + Canvas, + Primary, + Controls, + Unstyled, + Source, +} from '@storybook/blocks'; + +import * as TableStories from './tablev2.stories.tsx'; + + + +# Table + +Table is a powerful tool to display, filter and sort data. + +## Usage + +The Table needs only 2 props "columns" and "data" and either SingleSelectableContent or MultiSelectableContent as children to display. + + + +### SingleSelectableContent + +#### Basic Table + +Using the SingleSelectTable without passing the selectedId and onRowSelected props will render a simple table. + + + +#### Single Selectable Table + +Use this table when you want to have the possibility to select only one element of the table. +This allow, for example, to display more details or editing the selected row. + + + +### Multi Selectable Content + +Possibility to select one or more element of the table, with the possibility to select all elements with the checkbox on Header. + + + +### Search input + +The table component has a build-in search input. This subcomponent exist in 2 variations. +A standard search input and a search input using query params. +Both allow to search elements from the Table and display the number of corresponding elements. + + + + + +## Variations + +The Table component also accepts some optionnal props allowing for more customization. + +### Style Variations + +The Table component has no background and will take the color of its parent element. +It is however possible to change the line color thanks to the SeparationLineColor props + + + +### Status Variations + +The Table can take a status prop indicating the status of the data when fetched or queried. +The status accept 4 values 'loading' or 'idle', 'error', and 'success'. +'success' status allow the display of the data fed to the Table. +If the status is not defined then the Table behaves as if the status is 'success'. +If there is no data the Table will display an empty state : + + + +### Entity name + +You can pass an entityName props to table containing singular and plural for each define locale (currently english and french). +This entityName will be used by the search input and for the status messages. + + + + +## Playground + +This is a playground allowing to build and test the component. + + + diff --git a/stories/Table/tablev2.stories.tsx b/stories/Table/tablev2.stories.tsx new file mode 100644 index 0000000000..777468635c --- /dev/null +++ b/stories/Table/tablev2.stories.tsx @@ -0,0 +1,620 @@ +import React, { useState } from 'react'; +import { action } from '@storybook/addon-actions'; +import { Meta, StoryObj } from '@storybook/react'; +import { + Column, + Table, + TableProps, +} from '../../src/lib/components/tablev2/Tablev2.component'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { CellProps, Row } from 'react-table'; +import { Box, Button } from '../../src/lib/next'; +import { Stack, Wrap } from '../../src/lib'; +import { SingleSelectableContentProps } from '../../src/lib/components/tablev2/SingleSelectableContent'; +import { useArgs } from '@storybook/preview-api'; +import styled from 'styled-components'; + +const StyledBox = styled(Box)` + height: 150px; +`; + +/* ---------------------------------- DATA ---------------------------------- */ + +const data: Entry[] = [ + { + id: 1, + firstName: 'Sotiria-long-long-long-long-long', + lastName: 'Agathangelou', + age: undefined, + health: 'healthy', + }, + { + id: 2, + firstName: 'Stefania', + lastName: 'Evgenios', + age: 27, + health: 'warning', + }, + { + id: 3, + firstName: 'Yohann', + lastName: 'Rodolph', + age: 27, + health: 'critical', + }, + { + id: 4, + firstName: 'Ninette', + lastName: 'Caroline', + age: 31, + health: 'healthy', + }, +]; + +type Entry = { + id: number; + firstName: string; + lastName: string; + age?: number; + health: string; +}; + +const columns: Column[] = [ + { + Header: 'First Name', + accessor: 'firstName', + cellStyle: { + textAlign: 'left', + flex: 1, + }, + }, + { + Header: 'Last Name', + accessor: 'lastName', + cellStyle: { + flex: 1, + + textAlign: 'left', + }, + // disable the sorting on this column + disableSortBy: true, + }, + { + Header: 'Age', + accessor: 'age', + cellStyle: { + flex: 0.5, + textAlign: 'left', + }, + }, + { + Header: 'Health', + accessor: 'health', + sortType: 'health', + cellStyle: { + flex: 0.5, + textAlign: 'left', + }, + }, +]; +const getRowId = (row: Entry, relativeIndex: number) => { + return row.lastName + ' ' + row.firstName; +}; + +/* ---------------------------------- STORY --------------------------------- */ + +type SingleTable = TableProps & SingleSelectableContentProps; +type Story = StoryObj; + +const meta: Meta = { + title: 'Components/Data Display/Table', + decorators: [ + (Story) => ( + + + + + + ), + ], + component: Table, + render: ({ columns, data, ...rest }) => { + // const [selectedId, setSelectedId] = useState(undefined); + const [{ selectedId }, updateArgs] = useArgs(); + + return ( + + { + action('Table Row Clicked')(row); + updateArgs({ selectedId: row.id }); + }} + /> +
+ ); + }, + args: { + // @ts-ignore + columns: columns, + data: data, + defaultSortingKey: 'health', + }, + argTypes: { + separationLineVariant: { + control: { + type: 'select', + description: 'Background color', + defaultValue: 'backgroundLevel3', + }, + options: [ + 'backgroundLevel1', + 'backgroundLevel2', + 'backgroundLevel3', + 'backgroundLevel4', + ], + }, + }, +}; + +export default meta; + +export const Playground = {}; + +/* ---------------------------- SingleSelectTable --------------------------- */ + +export const SimpleTable: Story = { + render: () => { + return ( + + +
+ ); + }, +}; + +export const SingleSelectTable: Story = { + args: { + // @ts-ignore + getRowId: getRowId, + }, +}; + +export const TableStatus = { + render: (args) => { + return ( + + + + +
+
+ + + +
+
+ + + +
+
+
+ ); + }, +}; + +export const TableWithSearch: Story = { + render: () => { + const [value, setValue] = useState(''); + return ( + + { + setValue(value); + data.filter((data) => JSON.stringify(data).includes(value)); + }} + /> + + +
+ ); + }, +}; + +export const TableWithSearchQueryParams: Story = { + render: () => { + return ( + + { + data.filter((data) => JSON.stringify(data).includes(value)); + }} + /> + + +
+ ); + }, +}; + +export const TableWithEntityName: Story = { + render: (args) => { + const [{ selectedId }, updateArgs] = useArgs(); + return ( + + + { + action('Table Row Clicked')(row); + updateArgs({ selectedId: row.id }); + }} + /> +
+ ); + }, + argTypes: { + locale: { + options: ['en', 'fr'], + control: 'radio', + }, + }, +}; + +export const AsyncTable: Story = { + render: () => { + function DataComponent({ + data, + loading, + row, + }: { + row: Row; + loading: boolean; + data: string; + }) { + return loading ? ( + loading ... + ) : ( + {`${row.values.health} ${data}`} + ); + } + + function RowAsync({ row }: { row: Row }) { + const [loading, setLoading] = React.useState(true); + const [data, setData] = React.useState(''); + React.useEffect(() => { + const timer = setTimeout(() => { + setData('loaded async'); + setLoading(false); + }, 1000); + return () => { + clearTimeout(timer); + }; + }, []); + return ; + } + + const renderRowSubComponent = React.useCallback( + ({ row, ...rest }: CellProps) => { + return ; + }, + [], + ); + const columnAsync: Column[] = [ + { + Header: 'First Name', + accessor: 'firstName', + cellStyle: { + textAlign: 'left', + }, + }, + { + Header: 'Last Name', + accessor: 'lastName', + cellStyle: { + textAlign: 'left', + }, + }, + { + Header: 'Age', + accessor: 'age', + cellStyle: { + textAlign: 'left', + }, + }, + { + Header: 'Health', + accessor: 'health', + sortType: 'health', + cellStyle: { + textAlign: 'left', + }, + Cell: renderRowSubComponent, + }, + ]; + + return ( + + + +
+ ); + }, +}; + +export const OnBottomCallback: Story = { + render: () => { + const columns: Column<{ index: number; value: number }>[] = [ + { + Header: 'value', + accessor: 'value', + cellStyle: { + textAlign: 'left', + }, + }, + ]; + + const createData = (indexStart = 0) => { + const data: { index: number; value: number }[] = []; + + for (let i = 0; i < 100; i++) { + data.push({ + index: indexStart + i, + value: Math.floor(Math.random() * 1000), + }); + } + + return data; + }; + + const [randomData, setRandomData] = useState(createData()); + + const onBottom = () => { + action('onBottom'); + setRandomData([...randomData, ...createData(randomData.length)]); + }; + + return ( + + +
+ ); + }, +}; + +export const WithSeparationLine: Story = { + render: () => { + return ( + + +
+ ); + }, +}; + +/* ---------------------------- MultiSelect Table --------------------------- */ + +export const MultiSelectTable = { + render: () => { + return ( + + +
+ ); + }, +}; + +/* ---------------------------- Multi Table --------------------------- */ + +export const MultiTable: Story = { + render: () => { + const [data1, setData1] = useState([ + { + name: 'test', + volume: 1, + capacity: '1Gi', + }, + { + name: 'test', + volume: 1, + capacity: '1Gi', + }, + { + name: 'test', + volume: 1, + capacity: '1Gi', + }, + ]); + + const [data2, setData2] = useState([ + { + name: 'test', + volume: 1, + capacity: '1Gi', + }, + { + name: 'test', + volume: 1, + capacity: '1Gi', + }, + { + name: 'test', + volume: 1, + capacity: '1Gi', + }, + ]); + const columns2: Column<(typeof data2)[number]>[] = [ + { + Header: 'Name', + accessor: 'name', + cellStyle: { + textAlign: 'left', + width: 'unset', + flex: 1, + }, + }, + { + Header: 'Volume', + accessor: 'volume', + cellStyle: { + textAlign: 'left', + width: 'unset', + flex: 1, + }, + }, + { + Header: 'Capacity', + accessor: 'capacity', + cellStyle: { + textAlign: 'left', + width: 'unset', + flex: 1, + }, + }, + ]; + + const demo = () => { + setData1([ + { + name: 'test', + volume: 1, + capacity: '1Gi', + }, + { + name: 'test', + volume: 1, + capacity: '1Gi', + }, + ]); + + setData2([ + { + name: 'test', + volume: 1, + capacity: '2Gi', + }, + { + name: 'test', + volume: 1, + capacity: '1Gi', + }, + ]); + }; + + return ( + + + +
+ + +