Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(website): clarify table examples #3718

Merged
merged 1 commit into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading