Skip to content

Commit

Permalink
feat(website): clarify table examples (#3718)
Browse files Browse the repository at this point in the history
  • Loading branch information
gdostie committed May 14, 2024
1 parent 09ca081 commit 9ecc38b
Show file tree
Hide file tree
Showing 15 changed files with 442 additions and 300 deletions.
1 change: 1 addition & 0 deletions packages/mantine/src/components/table/Table.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 7 additions & 8 deletions packages/mantine/src/components/table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -148,6 +148,7 @@ export const Table = <T,>(props: TableProps<T> & {ref?: ForwardedRef<HTMLDivElem
minSize: defaultColumnSizing.minSize,
maxSize: defaultColumnSizing.maxSize,
},
rowCount: store.state.totalEntries,
...options,
});

Expand Down Expand Up @@ -215,7 +216,7 @@ export const Table = <T,>(props: TableProps<T> & {ref?: ForwardedRef<HTMLDivElem
<Box ref={mergedRef} {...others} {...getStyles('root')}>
<TableProvider<T> value={{getStyles, store, table, layouts, containerRef}}>
<Layout>
{!hasRows && !store.isFiltered && !loading ? (
{store.isVacant && !store.isFiltered ? (
noData
) : (
<>
Expand Down Expand Up @@ -246,18 +247,16 @@ export const Table = <T,>(props: TableProps<T> & {ref?: ForwardedRef<HTMLDivElem
) : (
<tr>
<td colSpan={table.getAllColumns().length}>
<TableLoading visible={loading}>{noData}</TableLoading>
<TableLoading visible={loading || !store.isFiltered}>
{noData}
</TableLoading>
</td>
</tr>
)}
</tbody>
</Box>
{footer}
{lastUpdated
? cloneElement(lastUpdated, {
dependencies: [data, ...(lastUpdated.props.dependencies ?? [])],
})
: null}
{lastUpdated}
</>
)}
</Layout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ const EmptyState = (props: {isFiltered: boolean}) =>
props.isFiltered ? <span data-testid="filtered-empty-state" /> : <span data-testid="empty-state" />;

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<RowData>();
const store = useTable<RowData>({initialState: {totalEntries: 0}});
return (
<Table data={[]} store={store} columns={columns}>
<Table.Header data-testid="table-header">header</Table.Header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ export const TablePagination: FunctionComponent<TablePaginationProps> = ({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) {
Expand Down
28 changes: 27 additions & 1 deletion packages/mantine/src/components/table/use-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ export interface TableStore<TData = unknown> {
* 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.
*/
Expand Down Expand Up @@ -174,6 +180,7 @@ export interface UseTableOptions<TData = unknown> {
const defaultOptions: UseTableOptions = {
enableRowSelection: true,
enableMultiRowSelection: false,
forceSelection: false,
};

const defaultState: Partial<TableState> = {
Expand All @@ -198,7 +205,10 @@ export const useTable = <TData>(userOptions: UseTableOptions<TData> = {}): Table
const [pagination, setPagination] = useState<TableState<TData>['pagination']>(
initialState.pagination as PaginationState,
);
const [totalEntries, setTotalEntries] = useState<TableState<TData>['totalEntries']>(initialState.totalEntries);
const [totalEntries, _setTotalEntries] = useState<TableState<TData>['totalEntries']>(initialState.totalEntries);
const [unfilteredTotalEntries, setUnfilteredTotalEntries] = useState<TableState<TData>['totalEntries']>(
initialState.totalEntries,
);
const [sorting, setSorting] = useState<TableState<TData>['sorting']>(initialState.sorting as SortingState);
const [globalFilter, setGlobalFilter] = useState<TableState<TData>['globalFilter']>(initialState.globalFilter);
const [predicates, setPredicates] = useState<TableState<TData>['predicates']>(initialState.predicates);
Expand All @@ -215,6 +225,21 @@ export const useTable = <TData>(userOptions: UseTableOptions<TData> = {}): 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('');
Expand Down Expand Up @@ -271,6 +296,7 @@ export const useTable = <TData>(userOptions: UseTableOptions<TData> = {}): Table
setRowSelection,
setColumnVisibility,
isFiltered,
isVacant,
clearFilters,
clearRowSelection,
getSelectedRows,
Expand Down
182 changes: 55 additions & 127 deletions packages/website/src/examples/layout/Table/Table.demo.tsx
Original file line number Diff line number Diff line change
@@ -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<IExampleRowData>();
const columnHelper = createColumnHelper<Person>();

/**
* Define your columns outside the component rendering the table
* (or memoize them) to avoid unnecessary render loops
*/
const columns: Array<ColumnDef<IExampleRowData>> = [
columnHelper.accessor('userId', {
header: 'User ID',
cell: (info) => info.row.original.userId,
const columns: Array<ColumnDef<Person>> = [
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<IExampleRowData>,
// or if you prefer an accordion behaviour
// Table.AccordionColumn as ColumnDef<IExampleRowData>,
];

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<IExampleRowData[]>(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<IExampleRowData>({
initialState: {predicates: {user: ''}},
const table = useTable<Person>({
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 (
<Table<IExampleRowData>
store={table}
data={data}
getRowId={({id}) => id.toString()}
columns={columns}
loading={loading}
getExpandChildren={(datum) => <Box py="xs">{datum.body}</Box>}
>
<Table<Person> store={table} data={data} columns={columns} getRowId={({id}) => id.toString()}>
<Table.Header>
<Table.Actions>{(datum: IExampleRowData) => <TableActions datum={datum} />}</Table.Actions>
<UserPredicate />
<Table.Filter placeholder="Search posts by title" />
<Table.Actions>
{(selectedRow: Person) => (
<>
<Button
variant="subtle"
onClick={() => alert(`Action 1 triggered for row: ${selectedRow.id}`)}
>
Action 1
</Button>
<Button
variant="subtle"
onClick={() => alert(`Action 2 triggered for row: ${selectedRow.id}`)}
>
Action 2
</Button>
</>
)}
</Table.Actions>
</Table.Header>
<Table.NoData>
<EmptyState isFiltered={table.isFiltered} clearFilters={table.clearFilters} />
</Table.NoData>
<Table.Footer>
<Table.PerPage />
<Table.Pagination />
</Table.Footer>
</Table>
);
};
export default Demo;

const EmptyState: FunctionComponent<{isFiltered: boolean; clearFilters: () => void}> = ({isFiltered, clearFilters}) =>
isFiltered ? (
<BlankSlate>
<Title order={4}>No data found for those filters</Title>
<Button onClick={clearFilters}>Clear filters</Button>
</BlankSlate>
) : (
<BlankSlate>
<Title order={4}>No Data</Title>
</BlankSlate>
);

const TableActions: FunctionComponent<{datum: IExampleRowData}> = ({datum}) => {
const actionCondition = datum.id % 2 === 0 ? true : false;
const pressedAction = () => alert('Edit action is triggered!');
return (
<>
{actionCondition ? (
<Button variant="subtle" onClick={pressedAction} leftSection={<EditSize16Px height={16} />}>
Edit
</Button>
) : null}
</>
);
export type Person = {
id: string;
firstName: string;
lastName: string;
age: number;
bio: string;
pic: string;
};

const UserPredicate: FunctionComponent = () => (
<Table.Predicate
id="user"
label="User"
data={[
{
value: '',
label: 'All',
},
{value: '1', label: '1'},
{value: '2', label: '2'},
{value: '3', label: '3'},
{value: '4', label: '4'},
{value: '5', label: '5'},
{value: '6', label: '6'},
{value: '7', label: '7'},
{value: '8', label: '8'},
{value: '9', label: '9'},
{value: '10', label: '10'},
]}
/>
);
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}),
}));
Loading

0 comments on commit 9ecc38b

Please sign in to comment.