diff --git a/packages/mantine/src/components/table/Table.module.css b/packages/mantine/src/components/table/Table.module.css index ae2417ae55..d9a1b4137e 100644 --- a/packages/mantine/src/components/table/Table.module.css +++ b/packages/mantine/src/components/table/Table.module.css @@ -37,6 +37,7 @@ background-color: var(--mantine-color-gray-1); padding: var(--mantine-spacing-sm) var(--mantine-spacing-xl); position: relative; + min-height: 69px; } .headerGridInner { diff --git a/packages/mantine/src/components/table/Table.tsx b/packages/mantine/src/components/table/Table.tsx index a3a2ccc6e0..a7867f8daf 100644 --- a/packages/mantine/src/components/table/Table.tsx +++ b/packages/mantine/src/components/table/Table.tsx @@ -9,7 +9,7 @@ import { useReactTable, } from '@tanstack/react-table'; import isEqual from 'fast-deep-equal'; -import {Children, ForwardedRef, ReactElement, cloneElement, useRef} from 'react'; +import {Children, ForwardedRef, ReactElement, useRef} from 'react'; import {CustomComponentThemeExtend, identity} from '../../utils'; import classes from './Table.module.css'; import {TableLayout, TableProps} from './Table.types'; @@ -148,6 +148,7 @@ export const Table = (props: TableProps & {ref?: ForwardedRef(props: TableProps & {ref?: ForwardedRef value={{getStyles, store, table, layouts, containerRef}}> - {!hasRows && !store.isFiltered && !loading ? ( + {store.isVacant && !store.isFiltered ? ( noData ) : ( <> @@ -246,18 +247,16 @@ export const Table = (props: TableProps & {ref?: ForwardedRef - {noData} + + {noData} + )} {footer} - {lastUpdated - ? cloneElement(lastUpdated, { - dependencies: [data, ...(lastUpdated.props.dependencies ?? [])], - }) - : null} + {lastUpdated} )} diff --git a/packages/mantine/src/components/table/__tests__/Table.spec.tsx b/packages/mantine/src/components/table/__tests__/Table.spec.tsx index e4345cf143..d33f485d17 100644 --- a/packages/mantine/src/components/table/__tests__/Table.spec.tsx +++ b/packages/mantine/src/components/table/__tests__/Table.spec.tsx @@ -18,10 +18,10 @@ const EmptyState = (props: {isFiltered: boolean}) => props.isFiltered ? : ; describe('Table', () => { - describe('when it has no data', () => { + describe('when it is vacant', () => { it('hides the footer and header if the table is not filtered', () => { const Fixture = () => { - const store = useTable(); + const store = useTable({initialState: {totalEntries: 0}}); return ( header diff --git a/packages/mantine/src/components/table/table-pagination/TablePagination.tsx b/packages/mantine/src/components/table/table-pagination/TablePagination.tsx index 8cb1b3c7bb..1ecd213722 100644 --- a/packages/mantine/src/components/table/table-pagination/TablePagination.tsx +++ b/packages/mantine/src/components/table/table-pagination/TablePagination.tsx @@ -14,10 +14,7 @@ export const TablePagination: FunctionComponent = ({onPage containerRef.current.scrollIntoView({behavior: 'smooth'}); }; - const total = - store.state.totalEntries == null - ? table.getPageCount() - : Math.ceil(store.state.totalEntries / store.state.pagination.pageSize); + const total = table.getPageCount(); useDidUpdate(() => { if (store.state.pagination.pageIndex + 1 > total && total > 0) { diff --git a/packages/mantine/src/components/table/use-table.ts b/packages/mantine/src/components/table/use-table.ts index 3630265a9c..03e529adbf 100644 --- a/packages/mantine/src/components/table/use-table.ts +++ b/packages/mantine/src/components/table/use-table.ts @@ -115,6 +115,12 @@ export interface TableStore { * Whether the table is currently filtered. */ isFiltered: boolean; + /** + * Whether the table has data when unfiltered. + * + * This is derived from the totalEntries so make sure you set that number correctly, even if you're using a client side table. + */ + isVacant: boolean; /** * Clear currently applied filters. */ @@ -174,6 +180,7 @@ export interface UseTableOptions { const defaultOptions: UseTableOptions = { enableRowSelection: true, enableMultiRowSelection: false, + forceSelection: false, }; const defaultState: Partial = { @@ -198,7 +205,10 @@ export const useTable = (userOptions: UseTableOptions = {}): Table const [pagination, setPagination] = useState['pagination']>( initialState.pagination as PaginationState, ); - const [totalEntries, setTotalEntries] = useState['totalEntries']>(initialState.totalEntries); + const [totalEntries, _setTotalEntries] = useState['totalEntries']>(initialState.totalEntries); + const [unfilteredTotalEntries, setUnfilteredTotalEntries] = useState['totalEntries']>( + initialState.totalEntries, + ); const [sorting, setSorting] = useState['sorting']>(initialState.sorting as SortingState); const [globalFilter, setGlobalFilter] = useState['globalFilter']>(initialState.globalFilter); const [predicates, setPredicates] = useState['predicates']>(initialState.predicates); @@ -215,6 +225,21 @@ export const useTable = (userOptions: UseTableOptions = {}): Table !!dateRange?.[0] || !!dateRange?.[1]; + const isVacant = unfilteredTotalEntries === 0; + + const setTotalEntries: typeof _setTotalEntries = useCallback( + (updater) => { + _setTotalEntries((old) => { + const newTotalEntries = updater instanceof Function ? updater(old) : updater; + if (!isFiltered) { + setUnfilteredTotalEntries(newTotalEntries); + } + return newTotalEntries; + }); + }, + [isFiltered], + ); + const clearFilters = useCallback(() => { setPredicates(initialState.predicates); setGlobalFilter(''); @@ -271,6 +296,7 @@ export const useTable = (userOptions: UseTableOptions = {}): Table setRowSelection, setColumnVisibility, isFiltered, + isVacant, clearFilters, clearRowSelection, getSelectedRows, diff --git a/packages/website/src/examples/layout/Table/Table.demo.tsx b/packages/website/src/examples/layout/Table/Table.demo.tsx index ecd2486723..3e00e954f5 100644 --- a/packages/website/src/examples/layout/Table/Table.demo.tsx +++ b/packages/website/src/examples/layout/Table/Table.demo.tsx @@ -1,156 +1,84 @@ -import {BlankSlate, Box, Button, ColumnDef, createColumnHelper, Table, Title, useTable} from '@coveord/plasma-mantine'; -import {EditSize16Px} from '@coveord/plasma-react-icons'; -import {FunctionComponent, useEffect, useState} from 'react'; +import {Button, ColumnDef, createColumnHelper, Table, useTable} from '@coveord/plasma-mantine'; +import {faker} from '@faker-js/faker'; +import {useMemo} from 'react'; -interface IExampleRowData { - userId: number; - id: number; - title: string; - body: string; -} - -const columnHelper = createColumnHelper(); +const columnHelper = createColumnHelper(); /** * Define your columns outside the component rendering the table * (or memoize them) to avoid unnecessary render loops */ -const columns: Array> = [ - columnHelper.accessor('userId', { - header: 'User ID', - cell: (info) => info.row.original.userId, +const columns: Array> = [ + columnHelper.accessor('firstName', { + header: 'First name', + enableSorting: false, }), - columnHelper.accessor('id', { - header: 'Post ID', - cell: (info) => info.row.original.id, + columnHelper.accessor('lastName', { + header: 'Last name', + enableSorting: false, }), - columnHelper.accessor('title', { - header: 'Title', - cell: (info) => info.row.original.title, + columnHelper.accessor('age', { + header: 'Age', + enableSorting: false, }), - Table.CollapsibleColumn as ColumnDef, - // or if you prefer an accordion behaviour - // Table.AccordionColumn as ColumnDef, ]; const Demo = () => { // How you manage your data and loading state is up to you // Just make sure data is a stable reference and isn't recreated on every render - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); + // Here for the sake of example we're building 10 rows of mock data + const data = useMemo(() => makeData(10), []); // `useTable` hook provides a table store. // The store contains the current state of the table and methods to update it. - const table = useTable({ - initialState: {predicates: {user: ''}}, + const table = useTable({ + initialState: {totalEntries: data.length}, }); - const fetchData = async () => { - setLoading(true); - // you can use the store state to build your query - const searchParams = new URLSearchParams({ - _sort: table.state.sorting?.[0]?.id ?? 'userId', - _order: table.state.sorting?.[0]?.desc ? 'desc' : 'asc', - _page: (table.state.pagination.pageIndex + 1).toString(), - _limit: table.state.pagination.pageSize.toString(), - userId: table.state.predicates.user, - title_like: table.state.globalFilter, - }); - if (table.state.predicates.user === '') { - searchParams.delete('userId'); - } - if (!table.state.globalFilter) { - searchParams.delete('title_like'); - } - try { - const response = await fetch(`https://jsonplaceholder.typicode.com/posts?${searchParams.toString()}`); - const body = await response.json(); - setData(body); - // The table needs to know the total number of entries to calculate the number of pages - table.setTotalEntries(Number(response.headers.get('x-total-count'))); - } catch (e) { - console.error(e); - } finally { - setLoading(false); - } - }; - - // refetch data when the table state you care about changes - useEffect(() => { - fetchData(); - }, [table.state.predicates, table.state.sorting, table.state.pagination, table.state.globalFilter]); - return ( - - store={table} - data={data} - getRowId={({id}) => id.toString()} - columns={columns} - loading={loading} - getExpandChildren={(datum) => {datum.body}} - > + store={table} data={data} columns={columns} getRowId={({id}) => id.toString()}> - {(datum: IExampleRowData) => } - - + + {(selectedRow: Person) => ( + <> + + + + )} + - - - - - - -
); }; export default Demo; -const EmptyState: FunctionComponent<{isFiltered: boolean; clearFilters: () => void}> = ({isFiltered, clearFilters}) => - isFiltered ? ( - - No data found for those filters - - - ) : ( - - No Data - - ); - -const TableActions: FunctionComponent<{datum: IExampleRowData}> = ({datum}) => { - const actionCondition = datum.id % 2 === 0 ? true : false; - const pressedAction = () => alert('Edit action is triggered!'); - return ( - <> - {actionCondition ? ( - - ) : null} - - ); +export type Person = { + id: string; + firstName: string; + lastName: string; + age: number; + bio: string; + pic: string; }; -const UserPredicate: FunctionComponent = () => ( - -); +const makeData = (len: number): Person[] => + Array(len) + .fill(0) + .map(() => ({ + id: faker.string.uuid(), + pic: faker.image.avatar(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + bio: faker.lorem.sentences({min: 1, max: 5}), + })); diff --git a/packages/website/src/examples/layout/Table/TableClientSide.demo.tsx b/packages/website/src/examples/layout/Table/TableClientSide.demo.tsx index 5fb81cb2b3..0bc33f8a7c 100644 --- a/packages/website/src/examples/layout/Table/TableClientSide.demo.tsx +++ b/packages/website/src/examples/layout/Table/TableClientSide.demo.tsx @@ -1,4 +1,6 @@ import { + BlankSlate, + Button, ColumnDef, createColumnHelper, FilterFn, @@ -7,6 +9,7 @@ import { getSortedRowModel, Table, TableProps, + Title, useTable, } from '@coveord/plasma-mantine'; import {faker} from '@faker-js/faker'; @@ -50,15 +53,21 @@ const columns: Array> = [ ]; const options: TableProps['options'] = { - globalFilterFn: fuzzyFilter, + // Specify the row models you need in the table options getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), + globalFilterFn: fuzzyFilter, // defines how the filter should operate }; const Demo = () => { const data = useMemo(() => makeData(45), []); - const table = useTable({initialState: {pagination: {pageSize: 5}}}); + const table = useTable({ + initialState: { + pagination: {pageSize: 5}, + totalEntries: data.length, + }, + }); return ( store={table} data={data} columns={columns} options={options} getRowId={({id}) => id}> @@ -68,6 +77,12 @@ const Demo = () => { + + + Nothing found for "{table.state.globalFilter}" + + + ); }; diff --git a/packages/website/src/examples/layout/Table/TableCollapsible.demo.tsx b/packages/website/src/examples/layout/Table/TableCollapsible.demo.tsx new file mode 100644 index 0000000000..e9d96b9cfc --- /dev/null +++ b/packages/website/src/examples/layout/Table/TableCollapsible.demo.tsx @@ -0,0 +1,65 @@ +import {Avatar, ColumnDef, createColumnHelper, Group, Table, useTable} from '@coveord/plasma-mantine'; +import {faker} from '@faker-js/faker'; +import {useMemo} from 'react'; + +const columnHelper = createColumnHelper(); + +const columns: Array> = [ + columnHelper.accessor('firstName', { + header: 'First name', + }), + columnHelper.accessor('lastName', { + header: 'Last name', + }), + columnHelper.accessor('age', { + header: 'Age', + }), + // Add the pre-built collapsible column at the end + Table.CollapsibleColumn as ColumnDef, + // or if you prefer an accordion behaviour + // Table.AccordionColumn as ColumnDef, +]; + +const Demo = () => { + const data = useMemo(() => makeData(10), []); + + const table = useTable({initialState: {totalEntries: data.length}}); + + return ( + + store={table} + data={data} + getRowId={({id}) => id.toString()} + columns={columns} + // Define the collapsible content with getExpandChildren + getExpandChildren={(datum) => ( + + + {datum.bio} + + )} + /> + ); +}; +export default Demo; + +export type Person = { + id: string; + firstName: string; + lastName: string; + age: number; + bio: string; + pic: string; +}; + +const makeData = (len: number): Person[] => + Array(len) + .fill(0) + .map(() => ({ + id: faker.string.uuid(), + pic: faker.image.avatar(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + bio: faker.lorem.sentences({min: 1, max: 5}), + })); diff --git a/packages/website/src/examples/layout/Table/TableColumnsSelector.demo.tsx b/packages/website/src/examples/layout/Table/TableColumnsSelector.demo.tsx index a3d5466ac1..7ccdfda300 100644 --- a/packages/website/src/examples/layout/Table/TableColumnsSelector.demo.tsx +++ b/packages/website/src/examples/layout/Table/TableColumnsSelector.demo.tsx @@ -1,4 +1,4 @@ -import {Box, ColumnDef, createColumnHelper, Table, Text, useTable} from '@coveord/plasma-mantine'; +import {ColumnDef, createColumnHelper, Table, Text, useTable} from '@coveord/plasma-mantine'; import {faker} from '@faker-js/faker'; import {useMemo} from 'react'; @@ -50,7 +50,6 @@ const columns: Array> = [ header: 'Hire Date', cell: (info) => info.row.original.hireDate?.toDateString(), }), - Table.CollapsibleColumn as ColumnDef, ]; const Demo = () => { @@ -62,13 +61,7 @@ const Demo = () => { }); return ( - employeeId?.toString()} - columns={columns} - getExpandChildren={(datum) => {datum.body}} - > +
employeeId?.toString()} columns={columns}> (); + +/** + * Define your columns outside the component rendering the table + * (or memoize them) to avoid unnecessary render loops + */ +const columns: Array> = [ + columnHelper.accessor('firstName', { + header: 'First name', + enableSorting: false, + }), + columnHelper.accessor('lastName', { + header: 'Last name', + enableSorting: false, + }), + columnHelper.accessor('lastActivity', { + header: 'Activity', + enableSorting: false, + cell: ({getValue}) => dayjs(getValue()).format('LLL'), + }), +]; + +const today: Date = dayjs().endOf('day').toDate(); +const previousDay: Date = dayjs().subtract(1, 'day').startOf('day').toDate(); +const previousWeek: Date = dayjs().subtract(1, 'week').startOf('day').toDate(); +const datePickerPresets: Record = { + lastDay: {label: 'Last 24 hours', range: [previousDay, today]}, + lastWeek: {label: 'Last week', range: [previousWeek, today]}, +}; + +const Demo = () => { + const data = useMemo(() => makeData(10), []); + const table = useTable({ + initialState: {totalEntries: data.length, dateRange: [previousWeek, today]}, + }); + + // we're filtering the data ourselves here for the example, + // but normally you would just send the date range to the backend + const filteredData = useMemo( + () => data.filter((person) => lastActivityDateFilter(person, table.state.dateRange)), + [table.state.dateRange, data], + ); + + return ( + store={table} data={filteredData} columns={columns} getRowId={({id}) => id.toString()}> + + + +
+ ); +}; +export default Demo; + +export type Person = { + id: string; + firstName: string; + lastName: string; + lastActivity: Date; +}; + +const makeData = (len: number): Person[] => + Array(len) + .fill(0) + .map(() => ({ + id: faker.string.uuid(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + lastActivity: faker.date.recent({days: 7}), + })); + +const lastActivityDateFilter = (row: Person, dateRange: DateRangePickerValue) => { + const lastActivity = row['lastActivity']; + + return dayjs(lastActivity).isAfter(dateRange[0]) && dayjs(lastActivity).isBefore(dateRange[1]); +}; diff --git a/packages/website/src/examples/layout/Table/TableEmptyState.demo.tsx b/packages/website/src/examples/layout/Table/TableEmptyState.demo.tsx index 7d9d967abf..9ea87d7a13 100644 --- a/packages/website/src/examples/layout/Table/TableEmptyState.demo.tsx +++ b/packages/website/src/examples/layout/Table/TableEmptyState.demo.tsx @@ -1,4 +1,4 @@ -import {BlankSlate, Button, ColumnDef, createColumnHelper, Table, Title, useTable} from '@coveord/plasma-mantine'; +import {BlankSlate, ColumnDef, createColumnHelper, Table, Title, useTable} from '@coveord/plasma-mantine'; import {NoContentSize32Px} from '@coveord/plasma-react-icons'; import {FunctionComponent} from 'react'; @@ -8,41 +8,30 @@ export type Person = { age: number; }; -const EmptyState: FunctionComponent<{isFiltered: boolean; clearFilters: () => void; filter: string}> = ({ - clearFilters, - isFiltered, - filter, -}) => - isFiltered ? ( - - No data found for filter "{filter}" - - - ) : ( - - - Hello Empty State - - ); +const EmptyState: FunctionComponent = () => ( + + + Empty State + This table is vacant, in other words it has no data even when no filter is applied. + +); const Demo = () => { - const table = useTable({initialState: {globalFilter: 'foo', pagination: {pageSize: 5}}}); + /** + * In order to determine properly when to display the empty state, the table needs to know the `totalEntries`. + * Be sure to set it either in the `initialState` or by using `store.setTotalEntries()` + */ + const data: Person[] = []; + const table = useTable({ + initialState: { + totalEntries: data.length, + }, + }); return ( - - - - + - - - -
); }; diff --git a/packages/website/src/examples/layout/Table/TableLayouts.demo.tsx b/packages/website/src/examples/layout/Table/TableLayouts.demo.tsx index a3c2ef9529..40c2f4d6eb 100644 --- a/packages/website/src/examples/layout/Table/TableLayouts.demo.tsx +++ b/packages/website/src/examples/layout/Table/TableLayouts.demo.tsx @@ -3,24 +3,18 @@ import { Button, ColumnDef, createColumnHelper, - FilterFn, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, Paper, renderTableCell, SimpleGrid, Table, TableLayout, TableLayoutProps, - TableProps, Title, useTable, useTableContext, } from '@coveord/plasma-mantine'; import {CardSize16Px} from '@coveord/plasma-react-icons'; import {faker} from '@faker-js/faker'; -import {rankItem} from '@tanstack/match-sorter-utils'; import {useMemo} from 'react'; const TableCards = (props: TableLayoutProps) => { @@ -78,14 +72,13 @@ CardLayout.Body = TableCards; CardLayout.Icon = CardSize16Px; const Demo = () => { - const data = useMemo(() => makeData(45), []); + const data = useMemo(() => makeData(10), []); const table = useTable({initialState: {pagination: {pageSize: 10}}}); return ( store={table} data={data} columns={columns} - options={options} getRowId={({id}) => id} layouts={[Table.Layouts.Rows, CardLayout]} doubleClickAction={(person) => alert(`Double clicked ${person.firstName}`)} @@ -99,10 +92,6 @@ const Demo = () => { )}
- - - - ); }; @@ -138,12 +127,3 @@ const makeData = (len: number): Person[] => lastName: faker.person.lastName(), age: faker.number.int(40), })); - -const fuzzyFilter: FilterFn = (row, columnId, value) => rankItem(row.getValue(columnId), value).passed; - -const options: TableProps['options'] = { - globalFilterFn: fuzzyFilter, - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), -}; diff --git a/packages/website/src/examples/layout/Table/TablePredicate.demo.tsx b/packages/website/src/examples/layout/Table/TablePredicate.demo.tsx new file mode 100644 index 0000000000..9a2c15da20 --- /dev/null +++ b/packages/website/src/examples/layout/Table/TablePredicate.demo.tsx @@ -0,0 +1,125 @@ +import {ColumnDef, createColumnHelper, Table, useTable} from '@coveord/plasma-mantine'; +import {faker} from '@faker-js/faker'; +import {useMemo} from 'react'; + +const columnHelper = createColumnHelper(); + +/** + * Define your columns outside the component rendering the table + * (or memoize them) to avoid unnecessary render loops + */ +const columns: Array> = [ + columnHelper.accessor('firstName', { + header: 'First name', + enableSorting: false, + }), + columnHelper.accessor('lastName', { + header: 'Last name', + enableSorting: false, + }), + columnHelper.accessor('status', { + header: 'Status', + enableSorting: false, + }), + columnHelper.accessor('age', { + header: 'Age', + enableSorting: false, + }), +]; + +const Demo = () => { + const data = useMemo(() => makeData(10), []); + const table = useTable({ + initialState: {totalEntries: data.length, predicates: {status: '', age: ''}}, + }); + + // we're filtering the data ourselves here for the example, + // but normally you would just send the predicate value to the backend + const filteredData = useMemo( + () => + data + .filter((person) => ageFilter(person, table.state.predicates)) + .filter((person) => statusFilter(person, table.state.predicates)), + [table.state.predicates, data], + ); + + return ( + store={table} data={filteredData} columns={columns} getRowId={({id}) => id.toString()}> + + + + + + ); +}; +export default Demo; + +export type Person = { + id: string; + firstName: string; + lastName: string; + age: number; + bio: string; + pic: string; + status: 'relationship' | 'complicated' | 'single'; +}; + +const makeData = (len: number): Person[] => + Array(len) + .fill(0) + .map(() => ({ + id: faker.string.uuid(), + pic: faker.image.avatar(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int({min: 1, max: 99}), + bio: faker.lorem.sentences({min: 1, max: 5}), + status: faker.helpers.shuffle(['relationship', 'complicated', 'single'])[0], + })); + +const ageFilter = (row: Person, predicates: Record) => { + const age = row['age']; + const filterValue = predicates['age']; + + switch (filterValue) { + case 'below20': + return age < 20; + case 'above60': + return age > 60; + case 'between20to60': + return age >= 20 && age <= 60; + default: + return true; + } +}; + +const statusFilter = (row: Person, predicates: Record) => { + const status = row['status']; + const filterValue = predicates['status']; + + return !filterValue || status === filterValue; +}; diff --git a/packages/website/src/examples/layout/Table/TableReactQuery.demo.tsx b/packages/website/src/examples/layout/Table/TableReactQuery.demo.tsx index 947b187ce9..e722203195 100644 --- a/packages/website/src/examples/layout/Table/TableReactQuery.demo.tsx +++ b/packages/website/src/examples/layout/Table/TableReactQuery.demo.tsx @@ -1,18 +1,6 @@ -import { - BlankSlate, - Box, - Button, - ColumnDef, - createColumnHelper, - DateRangePickerPreset, - Table, - Title, - useTable, -} from '@coveord/plasma-mantine'; -import {EditSize16Px} from '@coveord/plasma-react-icons'; -import dayjs from 'dayjs'; +import {BlankSlate, Button, ColumnDef, createColumnHelper, Table, Title, useTable} from '@coveord/plasma-mantine'; +import {keepPreviousData, useQuery} from '@tanstack/react-query'; import {FunctionComponent} from 'react'; -import {useQuery, keepPreviousData} from '@tanstack/react-query'; interface IExampleRowData { userId: number; @@ -23,10 +11,6 @@ interface IExampleRowData { const columnHelper = createColumnHelper(); -/** - * Define your columns outside the component rendering the table - * (or memoize them) to avoid unnecessary render loops - */ const columns: Array> = [ columnHelper.accessor('userId', { header: 'User ID', @@ -40,64 +24,47 @@ const columns: Array> = [ header: 'Title', cell: (info) => info.row.original.title, }), - Table.CollapsibleColumn as ColumnDef, - // or if you prefer an accordion behaviour - // Table.AccordionColumn as ColumnDef, ]; const Demo = () => { - const table = useTable({ - initialState: {dateRange: [previousDay, today], predicates: {user: ''}}, - }); + const table = useTable(); const query = useQuery({ - queryKey: [ - 'posts', - table.state.sorting, - table.state.pagination, - table.state.predicates, - table.state.globalFilter, - ], + queryKey: ['posts', table.state.sorting, table.state.pagination, table.state.globalFilter], queryFn: async () => { const searchParams = new URLSearchParams({ _sort: table.state.sorting?.[0]?.id ?? 'userId', _order: table.state.sorting?.[0]?.desc ? 'desc' : 'asc', _page: (table.state.pagination.pageIndex + 1).toString(), _limit: table.state.pagination.pageSize.toString(), - userId: table.state.predicates.user, title_like: table.state.globalFilter, }); - if (table.state.predicates.user === '') { - searchParams.delete('userId'); - } if (!table.state.globalFilter) { searchParams.delete('title_like'); } const response = await fetch(`https://jsonplaceholder.typicode.com/posts?${searchParams.toString()}`); const body = await response.json(); + // Give the total number of entries to the table + // This number is used to determine the total number of pages and the empty states table.setTotalEntries(Number(response.headers.get('x-total-count'))); return body; }, + // Keeping the previous data while fetching provides a smoother experience placeholderData: keepPreviousData, + // If you want to refresh the data every 10 seconds + // refetchInterval: 10 * 1000 }); return ( store={table} - loading={query.isLoading || query.isFetching} data={query.data} - getRowId={({id}) => id.toString()} columns={columns} - getExpandChildren={(datum) => {datum.body}} + loading={query.isLoading || query.isFetching} + getRowId={({id}) => id.toString()} > - - - {(datum: IExampleRowData) => } @@ -122,49 +89,3 @@ const NoData: FunctionComponent<{isFiltered: boolean; clearFilters: () => void}> No Data ); - -const today: Date = dayjs().startOf('day').toDate(); -const previousDay: Date = dayjs().subtract(1, 'day').endOf('day').toDate(); -const previousWeek: Date = dayjs().subtract(1, 'week').endOf('day').toDate(); - -const DatePickerPresets: Record = { - lastDay: {label: 'Last 24 hours', range: [previousDay, today]}, - lastWeek: {label: 'Last week', range: [previousWeek, today]}, -}; - -const TableActions: FunctionComponent<{datum: IExampleRowData}> = ({datum}) => { - const actionCondition = datum.id % 2 === 0 ? true : false; - const pressedAction = () => alert('Edit action is triggered!'); - return ( - <> - {actionCondition ? ( - - ) : null} - - ); -}; - -const UserPredicate: FunctionComponent = () => ( - -); diff --git a/packages/website/src/pages/layout/Table.tsx b/packages/website/src/pages/layout/Table.tsx index be11d54de3..3db7990b34 100644 --- a/packages/website/src/pages/layout/Table.tsx +++ b/packages/website/src/pages/layout/Table.tsx @@ -1,12 +1,15 @@ import {TableMetadata} from '@coveord/plasma-components-props-analyzer'; import TableDemo from '@examples/layout/Table/Table.demo?demo'; -import TableReactQuery from '@examples/layout/Table/TableReactQuery.demo?demo'; import TableClientSideDemo from '@examples/layout/Table/TableClientSide.demo?demo'; +import TableCollapsibleDemo from '@examples/layout/Table/TableCollapsible.demo?demo'; +import TableColumnsSelectorDemo from '@examples/layout/Table/TableColumnsSelector.demo?demo'; +import TableDateRangePickerDemo from '@examples/layout/Table/TableDateRangePicker.demo?demo'; import TableDisableRowSelection from '@examples/layout/Table/TableDisabledRowSelection.demo?demo'; import TableEmptyStateDemo from '@examples/layout/Table/TableEmptyState.demo?demo'; -import TableMultiSelectionDemo from '@examples/layout/Table/TableMultiSelection.demo?demo'; -import TableColumnsSelectorDemo from '@examples/layout/Table/TableColumnsSelector.demo?demo'; import TableLayoutsDemo from '@examples/layout/Table/TableLayouts.demo?demo'; +import TableMultiSelectionDemo from '@examples/layout/Table/TableMultiSelection.demo?demo'; +import TablePredicateDemo from '@examples/layout/Table/TablePredicate.demo?demo'; +import TableReactQuery from '@examples/layout/Table/TableReactQuery.demo?demo'; import {PageLayout} from '../../building-blocs/PageLayout'; @@ -18,18 +21,25 @@ const DemoPage = () => ( description="A table displays large quantities of items or data in a list format. Filtering features, date picker, collapsible rows and actions may be added." id="Table" propsMetadata={TableMetadata} - demo={} + demo={} examples={{ reactQuery: ( - + ), clientSide: ( ), + collapsible: , + predicate: , + datePicker: , emptyState: , multiSelect: (