Skip to content

Commit

Permalink
#236 DataTable - add retainDefaultSortBy prop for preserving defaul…
Browse files Browse the repository at this point in the history
…t sorting (#259)

Co-authored-by: Felix Beceic <fbeceic@croz.net>
  • Loading branch information
fbeceic and Felix Beceic authored Jun 5, 2024
1 parent 7129a4a commit ceef061
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 23 deletions.
67 changes: 59 additions & 8 deletions libs/data-display/src/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@

import * as React from "react";

import { every, find, flatMap, identity, isEqual, pickBy, sum } from "lodash";
import _ from "lodash";
import { Cell, Column, HeaderProps, Row, useExpanded, useRowSelect, useSortBy, useTable } from "react-table";
import { every, find, flatMap, slice, identity, isEqual, pickBy, sum, xorWith } from "lodash";
import {
Cell,
Column,
HeaderProps,
Row,
SortingRule,
useExpanded,
useRowSelect, useSortBy, useTable } from "react-table";

import { Card, CardHeaderProps } from "@tiller-ds/core";
import { Checkbox } from "@tiller-ds/form-elements";
Expand Down Expand Up @@ -55,6 +61,19 @@ export type DataTableProps<T extends object> = {
*/
defaultSortBy?: SortInfo[];

/**
* Controls table sorting behavior when clicking a column header by returning to a default sorted state defined by `defaultSortBy` prop when sort resets.
*
* By default, clicking a column header cycles through sorting states, and a sort reset returns the table to an unsorted state.
* Setting this prop to `true` prevents this default behavior.
*
* With this prop enabled, clicking the header of a column defined in `defaultSortBy` will toggle between ascending and descending order **even after a sort reset**.
* This allows you to maintain the initial sort order throughout user interaction.
*
* @defaultValue false
*/
retainDefaultSortBy?: boolean;

/**
* For getting each item's unique identifier on DataTable initialization.
* Ex. (item: Item) => item.id
Expand Down Expand Up @@ -368,6 +387,8 @@ type UseDataTable = [
isAllRowsSelected: boolean;

sortBy: SortInfo[];

defaultSortBy: SortInfo[];
},

DataTableHook,
Expand Down Expand Up @@ -442,7 +463,7 @@ export function useDataTable({ defaultSortBy = [] }: UseDataTableProps = {}): Us
);

const state = React.useMemo(
() => ({ selected, selectedCount, isAllRowsSelected, sortBy }),
() => ({ selected, selectedCount, isAllRowsSelected, sortBy, defaultSortBy }),
[selected, selectedCount, isAllRowsSelected, sortBy],
);

Expand All @@ -457,7 +478,7 @@ export function useDataTable({ defaultSortBy = [] }: UseDataTableProps = {}): Us
[updateSelected, updateSortBy, selected, isAllRowsSelected, toggleSelectAll],
);

return [state, hook];
return [state, hook] as UseDataTable;
}

type Operation = "sum" | "average";
Expand Down Expand Up @@ -505,8 +526,11 @@ function DataTable<T extends object>({
lastColumnFixed,
className,
multiSort = false,
retainDefaultSortBy,
...props
}: DataTableProps<T>) {
const tokens = useTokens("DataTable", props.tokens);

const primaryRow = findChild("DataTablePrimaryRow", children);
const secondaryRow = findChild("DataTableSecondaryRow", children);
const hasSecondaryColumns = React.isValidElement(secondaryRow);
Expand Down Expand Up @@ -565,7 +589,19 @@ function DataTable<T extends object>({
},
[getItemId],
);
const tokens = useTokens("DataTable", props.tokens);

const mapToSortingRules = (sortBy: SortInfo[], flipCols: string[]): SortingRule<T>[] => {
return sortBy.map((sortInfo) => ({
id: sortInfo.column,
desc: flipCols.includes(sortInfo.column)
? sortInfo.sortDirection !== "DESCENDING"
: sortInfo.sortDirection === "DESCENDING",
}));
};

const differenceOfSortingRules = (rule1: SortingRule<T>[], rule2: SortingRule<T>[]): SortingRule<T>[] =>
xorWith(rule1, rule2, (r1, r2) => r1.id === r2.id && r1.desc === r2.desc);

const {
getTableProps,
getTableBodyProps,
Expand All @@ -580,6 +616,21 @@ function DataTable<T extends object>({
{
columns,
data,
stateReducer: (newState, action, prevState) => {
const hasNoSort = newState.sortBy.length === 0;
const sortingDifference = differenceOfSortingRules(newState.sortBy, sortBy);

if (retainDefaultSortBy && action.type === "toggleSortBy" && (sortingDifference.length === 0 || hasNoSort)) {
const sortedCols = sortingDifference.map((rule) => rule.id);

return {
...newState,
sortBy: isEqual(prevState.sortBy, sortBy) ? mapToSortingRules(defaultSortBy, sortedCols) : sortBy,
};
}

return newState;
},
autoResetPage: false,
autoResetSelectedRows: false,
autoResetSortBy: false,
Expand Down Expand Up @@ -713,7 +764,7 @@ function DataTable<T extends object>({
rows.map((row, rowKey) => {
prepareRow(row);

const primaryCells = _.slice(row.cells, 0, columnChildrenSize);
const primaryCells = slice(row.cells, 0, columnChildrenSize);
const secondaryCells = hasSecondaryColumns
? row.cells.slice(columnChildrenSize, totalColumnChildrenSize)
: [];
Expand Down Expand Up @@ -1174,7 +1225,7 @@ const isConfigurationEqual = (prevChildren, nextChildren) => {
}
const getConfiguration = (children: Array<Exclude<React.ReactNode, boolean | null | undefined>>) =>
children.flatMap((child) => (React.isValidElement(child) ? child.props.type : undefined));
return _.isEqual(getConfiguration(prevChildArray), getConfiguration(nextChildArray));
return isEqual(getConfiguration(prevChildArray), getConfiguration(nextChildArray));
};

const MemoDataTable = React.memo(
Expand Down
11 changes: 8 additions & 3 deletions libs/data-display/src/useSortableDataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { useMemo } from "react";

import { useDataTable } from "./index";
import { SortInfo } from "./DataTable";

export default function useSortableDataTable<T, U extends keyof T>(initialData: T[], columnMapping: Record<U, string>) {
const [dataTableState, dataTableHook] = useDataTable();
export default function useSortableDataTable<T, U extends keyof T>(
initialData: T[],
columnMapping: Record<U, string>,
defaultSortBy?: SortInfo[],
) {
const [dataTableState, dataTableHook] = useDataTable({ defaultSortBy });

const generateSortedData = useMemo(() => {
const sortInstructions = dataTableState.sortBy;
Expand All @@ -15,7 +20,7 @@ export default function useSortableDataTable<T, U extends keyof T>(initialData:
return [...initialData].sort((a, b) => {
for (const sortInfo of sortInstructions) {
const columnKey = columnMapping[sortInfo.column];
const compareResult = sortInfo.sortDirection === "ASCENDING" ? -1 : 1;
const compareResult = sortInfo.sortDirection === "ASCENDING" ? 1 : -1;
const aValue = a[columnKey];
const bValue = b[columnKey];

Expand Down
62 changes: 50 additions & 12 deletions storybook/src/data-display/DataTable.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { range, slice } from "lodash";
import { withDesign } from "storybook-addon-designs";

import { Button, Card, IconButton, Link, Pagination, Typography, useLocalPagination } from "@tiller-ds/core";
import { DataTable, useDataTable, useLocalSummary, useSortableDataTable } from "@tiller-ds/data-display";
import { DataTable, SortInfo, useDataTable, useLocalSummary, useSortableDataTable } from "@tiller-ds/data-display";
import { Icon } from "@tiller-ds/icons";
import { DropdownMenu } from "@tiller-ds/menu";
import { defaultThemeConfig } from "@tiller-ds/theme";
Expand Down Expand Up @@ -1432,6 +1432,7 @@ export const WithDefaultAscendingSortByName = (args) => {
<DataTable data={sortedData} hook={dataTableHook} defaultSortBy={dataTableState.sortBy}>
<DataTable.Column header="ID" accessor="id" canSort={false} />
<DataTable.Column header="Name" accessor="name" canSort={true} />
<DataTable.Column header="Surname" accessor="surname" canSort={true} />
</DataTable>
);
};
Expand All @@ -1443,26 +1444,63 @@ export const WithDefaultAscendingSortUsingHook = () => {
surname: "surname",
};

const { dataTableHook, sortedData } = useSortableDataTable(allData || [], columnMapping);
const defaultSortBy: SortInfo[] = [
{
column: "name",
sortDirection: "DESCENDING",
},
{
column: "surname",
sortDirection: "DESCENDING",
},
];

const { dataTableHook, sortedData, dataTableState } = useSortableDataTable(
allData || [],
columnMapping,
defaultSortBy,
);

return (
<DataTable data={sortedData} hook={dataTableHook} defaultSortBy={dataTableState.sortBy}>
<DataTable.Column header="ID" accessor="id" canSort={false} />
<DataTable.Column header="Name" accessor="name" canSort={true} />
<DataTable.Column header="Surname" accessor="surname" canSort={true} />
</DataTable>
);
};

export const WithDefaultAscendingSortWithRetainedInitialSort = () => {
// incl-code
const columnMapping = {
name: "name",
surname: "surname",
};

const defaultSortBy: SortInfo[] = [
{
column: "name",
sortDirection: "DESCENDING",
},
];

const { dataTableHook, sortedData, dataTableState } = useSortableDataTable(
allData || [],
columnMapping,
defaultSortBy,
);

return (
<DataTable
data={sortedData}
hook={dataTableHook}
defaultSortBy={[
{
column: "name",
sortDirection: "ASCENDING",
},
{
column: "surname",
sortDirection: "ASCENDING",
},
]}
defaultSortBy={dataTableState.defaultSortBy}
retainDefaultSortBy={true}
>
<DataTable.Column header="ID" accessor="id" canSort={false} />
<DataTable.Column header="Name" accessor="name" canSort={true} />
<DataTable.Column header="Surname" accessor="surname" canSort={true} />
<DataTable.Column header="Applied for" accessor="appliedFor" canSort={true} />
</DataTable>
);
};
Expand Down

0 comments on commit ceef061

Please sign in to comment.