Skip to content

Commit

Permalink
Merge pull request #317 from icflorescu/next
Browse files Browse the repository at this point in the history
Allow idAccessor to be a function
  • Loading branch information
icflorescu authored Jun 6, 2023
2 parents 0acdd00 + 903647a commit f065627
Show file tree
Hide file tree
Showing 13 changed files with 537 additions and 518 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 2.5.6 (2023-06-06)

- Allow `idAccessor` to be a string **or** a function, in order to support composite keys (issue #315)

## 2.5.5 (2023-06-01)

- Improve filtering support documentation
Expand Down
48 changes: 48 additions & 0 deletions docs/examples/NonStandardRecordIdsFunctionExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { DataTable } from 'mantine-datatable';

export default function NonStandardRecordIdsFunctionExample() {
return (
<DataTable
withBorder
withColumnBorders
striped
records={[
{ bookTitle: 'The Fellowship of the Ring', character: 'Frodo Baggins', bornIn: 2968 },
// example-skip more records
{ bookTitle: 'The Fellowship of the Ring', character: 'Samwise Gamgee', bornIn: 2980 },
{ bookTitle: 'The Fellowship of the Ring', character: 'Meriadoc Brandybuck', bornIn: 2982 },
{ bookTitle: 'The Fellowship of the Ring', character: 'Peregrin Took', bornIn: 2990 },
{ bookTitle: 'The Fellowship of the Ring', character: 'Gandalf', bornIn: 1000 },
{ bookTitle: 'The Fellowship of the Ring', character: 'Aragorn son of Arathorn', bornIn: 2931 },
{ bookTitle: 'The Fellowship of the Ring', character: 'Legolas', bornIn: 2931 },
{ bookTitle: 'The Fellowship of the Ring', character: 'Gimli son of Gloin', bornIn: 2879 },
{ bookTitle: 'The Fellowship of the Ring', character: 'Boromir son of Denethor', bornIn: 2978 },
{ bookTitle: 'The Two Towers', character: 'Frodo Baggins', bornIn: 2968 },
{ bookTitle: 'The Two Towers', character: 'Samwise Gamgee', bornIn: 2980 },
{ bookTitle: 'The Two Towers', character: 'Meriadoc Brandybuck', bornIn: 2982 },
{ bookTitle: 'The Two Towers', character: 'Peregrin Took', bornIn: 2990 },
{ bookTitle: 'The Two Towers', character: 'Gandalf', bornIn: 1000 },
{ bookTitle: 'The Two Towers', character: 'Aragorn son of Arathorn', bornIn: 2931 },
{ bookTitle: 'The Two Towers', character: 'Legolas', bornIn: 2931 },
{ bookTitle: 'The Two Towers', character: 'Gimli son of Gloin', bornIn: 2879 },
{ bookTitle: 'The Two Towers', character: 'Boromir son of Denethor', bornIn: 2978 },
{ bookTitle: 'The Return of the King', character: 'Frodo Baggins', bornIn: 2968 },
{ bookTitle: 'The Return of the King', character: 'Samwise Gamgee', bornIn: 2980 },
{ bookTitle: 'The Return of the King', character: 'Meriadoc Brandybuck', bornIn: 2982 },
{ bookTitle: 'The Return of the King', character: 'Peregrin Took', bornIn: 2990 },
{ bookTitle: 'The Return of the King', character: 'Gandalf', bornIn: 1000 },
// example-resume
]}
columns={[
{ accessor: 'character', width: '100%' },
{ accessor: 'bornIn', textAlignment: 'right' },
{ accessor: 'bookTitle', noWrap: true },
]}
/**
* Non-standard record ID.
* In this case we're using a function that returns a "composite" ID
*/
idAccessor={({ bookTitle, character }) => `${bookTitle}:${character}`}
/>
);
}
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mantine-datatable-docs",
"version": "2.5.5",
"version": "2.5.6",
"description": "Docs website for mantine-datatable; see ../package/package.json for more info",
"private": true,
"scripts": {
Expand Down
28 changes: 23 additions & 5 deletions docs/pages/examples/non-standard-record-ids.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@ import { Code, Container } from '@mantine/core';
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import CodeBlock from '~/components/CodeBlock';
import PageNavigation from '~/components/PageNavigation';
import PageSubtitle from '~/components/PageSubtitle';
import PageText from '~/components/PageText';
import PageTitle from '~/components/PageTitle';
import NonStandardRecordIdsExample from '~/examples/NonStandardRecordIdsExample';
import NonStandardRecordIdsFunctionExample from '~/examples/NonStandardRecordIdsFunctionExample';
import NonStandardRecordIdsStringExample from '~/examples/NonStandardRecordIdsStringExample';
import allPromiseProps from '~/lib/allPromiseProps';
import readCodeExample from '~/lib/readCodeExample';

const PATH = 'examples/non-standard-record-ids';

export const getStaticProps: GetStaticProps<{ code: string }> = async () => ({
props: { code: (await readCodeExample('examples/NonStandardRecordIdsExample.tsx')) as string },
export const getStaticProps: GetStaticProps<{
code: Record<'string' | 'function', string>;
}> = async () => ({
props: {
code: await allPromiseProps({
string: readCodeExample('examples/NonStandardRecordIdsStringExample.tsx') as Promise<string>,
function: readCodeExample('examples/NonStandardRecordIdsFunctionExample.tsx') as Promise<string>,
}),
},
});

export default function Page({ code }: InferGetStaticPropsType<typeof getStaticProps>) {
Expand All @@ -27,9 +37,17 @@ export default function Page({ code }: InferGetStaticPropsType<typeof getStaticP
You can override the default ID property name by adding an <Code>idAccessor</Code> property on the{' '}
<Code>DataTable</Code> like so:
</PageText>
<CodeBlock language="typescript" content={code} />
<CodeBlock language="typescript" content={code.string} />
<PageText>The code above will produce the following result:</PageText>
<NonStandardRecordIdsExample />
<NonStandardRecordIdsStringExample />
<PageSubtitle value="Using functions to generate composite record IDs" />
<PageText>
You can also use a function to generate record IDs. This is useful for composite IDs, for example, when you need
to generate a unique ID based on multiple record properties:
</PageText>
<CodeBlock language="typescript" content={code.function} />
<PageText>The code above will produce the following result:</PageText>
<NonStandardRecordIdsFunctionExample />
<PageNavigation of={PATH} />
</Container>
);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mantine-datatable-turborepo",
"version": "2.5.5",
"version": "2.5.6",
"description": "This is a monorepo; see package/package.json for more info",
"private": true,
"workspaces": [
Expand Down
24 changes: 12 additions & 12 deletions package/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import DataTableRowMenuItem from './DataTableRowMenuItem';
import DataTableScrollArea from './DataTableScrollArea';
import { useLastSelectionChangeIndex, useRowContextMenu, useRowExpansion } from './hooks';
import type { DataTableProps } from './types';
import { differenceBy, getValueAtPath, humanize, uniqBy, useIsomorphicLayoutEffect } from './utils';
import { differenceBy, getRecordId, humanize, uniqBy, useIsomorphicLayoutEffect } from './utils';

const EMPTY_OBJECT = {};

Expand Down Expand Up @@ -258,14 +258,14 @@ export default function DataTable<T>({
);

const recordsLength = records?.length;
const recordIds = records?.map((record) => getValueAtPath(record, idAccessor));
const recordIds = records?.map((record) => getRecordId(record, idAccessor));
const selectionColumnVisible = !!selectedRecords;
const selectedRecordIds = selectedRecords?.map((record) => getValueAtPath(record, idAccessor));
const selectedRecordIds = selectedRecords?.map((record) => getRecordId(record, idAccessor));
const hasRecordsAndSelectedRecords =
recordIds !== undefined && selectedRecordIds !== undefined && selectedRecordIds.length > 0;

const selectableRecords = isRecordSelectable ? records?.filter(isRecordSelectable) : records;
const selectableRecordIds = selectableRecords?.map((record) => getValueAtPath(record, idAccessor));
const selectableRecordIds = selectableRecords?.map((record) => getRecordId(record, idAccessor));

const allSelectableRecordsSelected =
hasRecordsAndSelectedRecords && selectableRecordIds!.every((id) => selectedRecordIds.includes(id));
Expand All @@ -275,8 +275,8 @@ export default function DataTable<T>({
const handleHeaderSelectionChange = useCallback(() => {
onSelectedRecordsChange?.(
allSelectableRecordsSelected
? selectedRecords.filter((record) => !selectableRecordIds!.includes(getValueAtPath(record, idAccessor)))
: uniqBy([...selectedRecords, ...selectableRecords!], (record) => getValueAtPath(record, idAccessor))
? selectedRecords.filter((record) => !selectableRecordIds!.includes(getRecordId(record, idAccessor)))
: uniqBy([...selectedRecords, ...selectableRecords!], (record) => getRecordId(record, idAccessor))
);
}, [
allSelectableRecordsSelected,
Expand Down Expand Up @@ -352,7 +352,7 @@ export default function DataTable<T>({
<tbody ref={bodyRef}>
{recordsLength ? (
records.map((record, recordIndex) => {
const recordId = getValueAtPath(record, idAccessor);
const recordId = getRecordId(record, idAccessor);
const isSelected = selectedRecordIds?.includes(recordId) || false;

let showContextMenuOnClick = false;
Expand Down Expand Up @@ -385,14 +385,14 @@ export default function DataTable<T>({
);
onSelectedRecordsChange(
isSelected
? differenceBy(selectedRecords, targetRecords, (r) => getValueAtPath(r, idAccessor))
: uniqBy([...selectedRecords, ...targetRecords], (r) => getValueAtPath(r, idAccessor))
? differenceBy(selectedRecords, targetRecords, (r) => getRecordId(r, idAccessor))
: uniqBy([...selectedRecords, ...targetRecords], (r) => getRecordId(r, idAccessor))
);
} else {
onSelectedRecordsChange(
isSelected
? selectedRecords.filter((record) => getValueAtPath(record, idAccessor) !== recordId)
: uniqBy([...selectedRecords, record], (record) => getValueAtPath(record, idAccessor))
? selectedRecords.filter((record) => getRecordId(record, idAccessor) !== recordId)
: uniqBy([...selectedRecords, record], (record) => getRecordId(record, idAccessor))
);
}
setLastSelectionChangeIndex(recordIndex);
Expand Down Expand Up @@ -435,7 +435,7 @@ export default function DataTable<T>({
onCellClick={onCellClick}
onContextMenu={handleContextMenu}
contextMenuVisible={
rowContextMenuInfo ? getValueAtPath(rowContextMenuInfo.record, idAccessor) === recordId : false
rowContextMenuInfo ? getRecordId(rowContextMenuInfo.record, idAccessor) === recordId : false
}
expansion={rowExpansionInfo}
className={rowClassName}
Expand Down
46 changes: 27 additions & 19 deletions package/DataTableHeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Box, Center, createStyles, Group, ActionIcon, Popover, Slider, type MantineTheme, type Sx } from '@mantine/core';
import { ActionIcon, Box, Center, Group, Popover, createStyles, type MantineTheme, type Sx } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconArrowsVertical, IconArrowUp, IconFilter } from '@tabler/icons-react';
import { IconArrowUp, IconArrowsVertical, IconFilter } from '@tabler/icons-react';
import type { CSSProperties, ReactNode } from 'react';
import { type BaseSyntheticEvent } from 'react';
import type { DataTableColumn, DataTableSortProps } from './types';
import { humanize, useMediaQueryStringOrFunction } from './utils';
import { type BaseSyntheticEvent } from 'react';

const useStyles = createStyles((theme) => ({
sortableColumnHeader: {
Expand Down Expand Up @@ -52,19 +52,25 @@ type DataTableHeaderCellProps<T> = {
onSortStatusChange: DataTableSortProps['onSortStatusChange'];
} & Pick<DataTableColumn<T>, 'accessor' | 'sortable' | 'textAlignment' | 'width' | 'filter' | 'filtering'>;

function Filter<T>({ children, isActive }: { children: DataTableColumn<T>['filter'], isActive: boolean }) {
const [isOpen, {close, toggle}] = useDisclosure(false);
function Filter<T>({ children, isActive }: { children: DataTableColumn<T>['filter']; isActive: boolean }) {
const [isOpen, { close, toggle }] = useDisclosure(false);

return <Popover withArrow withinPortal shadow="md" opened={isOpen} onClose={close} trapFocus>
<Popover.Target>
<ActionIcon onClick={e => { e.preventDefault(); toggle(); }} variant={isActive ? 'default' : 'subtle'}>
<IconFilter size={14} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>
{typeof children === 'function' ? children({ close }) : children}
</Popover.Dropdown>
</Popover>
return (
<Popover withArrow withinPortal shadow="md" opened={isOpen} onClose={close} trapFocus>
<Popover.Target>
<ActionIcon
onClick={(e) => {
e.preventDefault();
toggle();
}}
variant={isActive ? 'default' : 'subtle'}
>
<IconFilter size={14} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>{typeof children === 'function' ? children({ close }) : children}</Popover.Dropdown>
</Popover>
);
}

export default function DataTableHeaderCell<T>({
Expand All @@ -81,7 +87,7 @@ export default function DataTableHeaderCell<T>({
sortStatus,
onSortStatusChange,
filter,
filtering
filtering,
}: DataTableHeaderCellProps<T>) {
const { cx, classes } = useStyles();
if (!useMediaQueryStringOrFunction(visibleMediaQuery)) return null;
Expand All @@ -90,7 +96,9 @@ export default function DataTableHeaderCell<T>({
const sortAction =
sortable && onSortStatusChange
? (e?: BaseSyntheticEvent) => {
if (e?.defaultPrevented) { return; }
if (e?.defaultPrevented) {
return;
}
onSortStatusChange({
columnAccessor: accessor,
direction:
Expand Down Expand Up @@ -126,7 +134,7 @@ export default function DataTableHeaderCell<T>({
{text}
</Box>
{sortable || sortStatus?.columnAccessor === accessor ? (
<>
<>
{sortStatus?.columnAccessor === accessor ? (
<Center
className={cx(classes.sortableColumnHeaderIcon, {
Expand All @@ -142,7 +150,7 @@ export default function DataTableHeaderCell<T>({
{sortIcons?.unsorted || <IconArrowsVertical size={14} />}
</Center>
)}
</>
</>
) : null}
{filter ? <Filter isActive={!!filtering}>{filter}</Filter> : null}
</Group>
Expand Down
16 changes: 8 additions & 8 deletions package/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useTimeout } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { useEffect, useState, type Key } from 'react';
import type { DataTableRowExpansionProps } from './types';
import { getValueAtPath } from './utils';
import { getRecordId } from './utils';

export function useLastSelectionChangeIndex(recordIds: unknown[] | undefined) {
const [lastSelectionChangeIndex, setLastSelectionChangeIndex] = useState<number | null>(null);
Expand Down Expand Up @@ -33,15 +33,15 @@ export function useRowExpansion<T>({
}: {
rowExpansion?: DataTableRowExpansionProps<T>;
records: T[] | undefined;
idAccessor: string;
idAccessor: string | ((record: T) => Key);
}) {
let initiallyExpandedRecordIds: unknown[] = [];
if (rowExpansion && records) {
const { trigger, allowMultiple, initiallyExpanded } = rowExpansion;
if (records && trigger === 'always') {
initiallyExpandedRecordIds = records.map((r) => getValueAtPath(r, idAccessor));
initiallyExpandedRecordIds = records.map((r) => getRecordId(r, idAccessor));
} else if (initiallyExpanded) {
initiallyExpandedRecordIds = records.filter(initiallyExpanded).map((r) => getValueAtPath(r, idAccessor));
initiallyExpandedRecordIds = records.filter(initiallyExpanded).map((r) => getRecordId(r, idAccessor));
if (!allowMultiple) {
initiallyExpandedRecordIds = [initiallyExpandedRecordIds[0]];
}
Expand All @@ -61,14 +61,14 @@ export function useRowExpansion<T>({
}

const collapseRow = (record: T) =>
setExpandedRecordIds?.(expandedRecordIds.filter((id) => id !== getValueAtPath(record, idAccessor)));
setExpandedRecordIds?.(expandedRecordIds.filter((id) => id !== getRecordId(record, idAccessor)));

return {
expandOnClick: trigger !== 'always' && trigger !== 'never',
isRowExpanded: (record: T) =>
trigger === 'always' ? true : expandedRecordIds.includes(getValueAtPath(record, idAccessor)),
trigger === 'always' ? true : expandedRecordIds.includes(getRecordId(record, idAccessor)),
expandRow: (record: T) => {
const recordId = getValueAtPath(record, idAccessor);
const recordId = getRecordId(record, idAccessor);
setExpandedRecordIds?.(allowMultiple ? [...expandedRecordIds, recordId] : [recordId]);
},
collapseRow,
Expand Down
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mantine-datatable",
"version": "2.5.5",
"version": "2.5.6",
"description": "Datatable component for Mantine UI, featuring asynchronous data loading support, pagination, multple rows selection, column sorting, custom cell data rendering, row context menu, row expansion and more",
"keywords": [
"ui",
Expand Down
8 changes: 5 additions & 3 deletions package/types/DataTableProps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DefaultProps, MantineShadow, MantineTheme, Sx, TableProps } from '@mantine/core';
import type { CSSProperties, MouseEvent, ReactNode, RefObject } from 'react';
import type { CSSProperties, Key, MouseEvent, ReactNode, RefObject } from 'react';
import type { DataTableCellClickHandler } from './DataTableCellClickHandler';
import type { DataTableColumn } from './DataTableColumn';
import type { DataTableContextMenuProps } from './DataTableContextMenuProps';
Expand Down Expand Up @@ -73,11 +73,13 @@ export type DataTableProps<T> = {
defaultColumnRender?: (record: T, index: number, accesor: string) => ReactNode;

/**
* Accessor to use as unique record key; you can use dot-notation for nested objects property drilling
* Accessor to use as unique record key; can be a string representing a property name
* or a function receiving the current record and returning a unique value.
* If you're providing a string, you can use dot-notation for nested objects property drilling
* (i.e. `department.name` or `department.company.name`);
* defaults to `id`
*/
idAccessor?: string;
idAccessor?: string | ((record: T) => Key);

/**
* Visible records; the `DataTable` component will try to infer its row type from here
Expand Down
9 changes: 8 additions & 1 deletion package/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMantineTheme, type MantineTheme } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { useEffect, useLayoutEffect } from 'react';
import { Key, useEffect, useLayoutEffect } from 'react';

export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;

Expand Down Expand Up @@ -44,3 +44,10 @@ export function getValueAtPath(obj: unknown, path: string) {
const pathArray = path.match(/([^[.\]])+/g) as string[];
return pathArray.reduce((prevObj: unknown, key) => prevObj && (prevObj as Record<string, unknown>)[key], obj);
}

/**
* Utility function that returns the record id using idAccessor
*/
export function getRecordId<T>(record: T, idAccessor: string | ((record: T) => Key)) {
return typeof idAccessor === 'string' ? getValueAtPath(record, idAccessor) : idAccessor(record);
}
Loading

0 comments on commit f065627

Please sign in to comment.