From 317cab4badeba6ff9328d4d55ba43a5f7f7b08b1 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Mon, 11 Mar 2024 15:17:18 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(react)=20add=20sorting=20on=20custom?= =?UTF-8?q?=20cells?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously sorting on custom cells was not possible because they were considered as "display cells" by React-Table, which is made for actions columns, which aren't sortable. Closes #296 #100 --- .changeset/tall-glasses-attack.md | 5 + .../DataGrid/SimpleDataGrid.spec.tsx | 95 ++++++++++++++++++- .../src/components/DataGrid/index.stories.tsx | 19 ++++ .../react/src/components/DataGrid/index.tsx | 7 ++ .../react/src/components/DataGrid/utils.tsx | 23 ++--- 5 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 .changeset/tall-glasses-attack.md diff --git a/.changeset/tall-glasses-attack.md b/.changeset/tall-glasses-attack.md new file mode 100644 index 0000000..b59dddd --- /dev/null +++ b/.changeset/tall-glasses-attack.md @@ -0,0 +1,5 @@ +--- +"@openfun/cunningham-react": minor +--- + +add sorting on custom columns diff --git a/packages/react/src/components/DataGrid/SimpleDataGrid.spec.tsx b/packages/react/src/components/DataGrid/SimpleDataGrid.spec.tsx index 85cac9f..e871da7 100644 --- a/packages/react/src/components/DataGrid/SimpleDataGrid.spec.tsx +++ b/packages/react/src/components/DataGrid/SimpleDataGrid.spec.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from "@testing-library/react"; import React, { useState } from "react"; import { faker } from "@faker-js/faker"; -import { getAllByRole, getByRole } from "@testing-library/dom"; +import { getAllByRole, getByRole, within } from "@testing-library/dom"; import { expect } from "vitest"; import userEvent from "@testing-library/user-event"; import { expectPaginationList } from ":/components/Pagination/utils"; @@ -418,6 +418,99 @@ describe("", () => { }); }); }); + it("should render a grid with working sortable custom columns", async () => { + const rows = Array.from(Array(23)) + .map(() => ({ + id: faker.string.uuid(), + firstName: faker.person.firstName(), + country: faker.location.country(), + })) + .sort((a, b) => a.firstName.localeCompare(b.firstName)); + render( + +

{row.country}

, + }, + ]} + rows={rows} + defaultPaginationParams={{ + pageSize: 10, + }} + defaultSortModel={[ + { + field: "firstName", + sort: "asc", + }, + ]} + /> +
, + ); + + const user = userEvent.setup(); + + // Verify first page rows are sorted by firstName ASC. + expect(screen.getAllByRole("row").length).toBe(11); + rows.slice(0, 10).forEach((row) => { + const element = screen.getByTestId(row.id); + const tds = getAllByRole(element, "cell"); + expect(tds.length).toBe(2); + expect(tds[0].textContent).toEqual(row.firstName); + }); + + // Sort by country ASC. + const table = screen.getByRole("table"); + const ths = getAllByRole(table, "columnheader"); + expect(ths.length).toBe(2); + expect(ths[1].textContent).toContain("Country"); + expect(ths[1].textContent).not.toContain("arrow_drop_up"); + expect(ths[1].textContent).not.toContain("arrow_drop_down"); + await user.click(ths[1].querySelector("div")!); + + // Verify first page rows are sorted by country ASC. + await waitFor(() => { + expect(ths[1].textContent).toContain("arrow_drop_up"); + [...rows] + .sort((a, b) => a.country.localeCompare(b.country)) + .slice(0, 10) + .forEach((row) => { + const element = screen.getByTestId(row.id); + const tds = getAllByRole(element, "cell"); + expect(tds.length).toBe(2); + expect(tds[0].textContent).toEqual(row.firstName); + within(tds[1]).getByRole("heading", { name: row.country, level: 1 }); + }); + }); + + // Sort by country DESC. + expect(ths.length).toBe(2); + expect(ths[1].textContent).toContain("Country"); + expect(ths[1].textContent).toContain("arrow_drop_up"); + expect(ths[1].textContent).not.toContain("arrow_drop_down"); + await user.click(ths[1].querySelector("div")!); + + await waitFor(() => { + expect(ths[1].textContent).toContain("arrow_drop_down"); + [...rows] + .sort((a, b) => a.country.localeCompare(b.country)) + .reverse() + .slice(0, 10) + .forEach((row) => { + const element = screen.getByTestId(row.id); + const tds = getAllByRole(element, "cell"); + expect(tds.length).toBe(2); + expect(tds[0].textContent).toEqual(row.firstName); + within(tds[1]).getByRole("heading", { name: row.country, level: 1 }); + }); + }); + }); it("should render a grid with non-sortable columns", async () => { const rows = Array.from(Array(23)) .map(() => ({ diff --git a/packages/react/src/components/DataGrid/index.stories.tsx b/packages/react/src/components/DataGrid/index.stories.tsx index 0cf2e12..c4a4bf2 100644 --- a/packages/react/src/components/DataGrid/index.stories.tsx +++ b/packages/react/src/components/DataGrid/index.stories.tsx @@ -195,6 +195,7 @@ export const FullServerSide = () => { lastName: faker.person.lastName(), email: faker.internet.email(), address: faker.location.streetAddress(), + country: faker.location.countryCode(), })), [], ); @@ -261,6 +262,24 @@ export const FullServerSide = () => { field: "address", headerName: "Address", }, + { + headerName: "Country", + field: "country", + renderCell: (context) => { + return ( + + Flag + {context.row.country} + + ); + }, + }, { id: "actions", renderCell: () => ( diff --git a/packages/react/src/components/DataGrid/index.tsx b/packages/react/src/components/DataGrid/index.tsx index 0f3edf8..a5003e6 100644 --- a/packages/react/src/components/DataGrid/index.tsx +++ b/packages/react/src/components/DataGrid/index.tsx @@ -30,12 +30,19 @@ export interface ColumnField { } export interface ColumnCustomCell { + id?: string; + field: string; + renderCell: (params: { row: T }) => React.ReactNode; +} +export interface ColumnDisplayCell { id: string; + field?: never; renderCell: (params: { row: T }) => React.ReactNode; } export type Column = ( | ColumnCustomCell + | ColumnDisplayCell | ColumnField ) & { headerName?: string; diff --git a/packages/react/src/components/DataGrid/utils.tsx b/packages/react/src/components/DataGrid/utils.tsx index 48a6276..61e79ad 100644 --- a/packages/react/src/components/DataGrid/utils.tsx +++ b/packages/react/src/components/DataGrid/utils.tsx @@ -1,4 +1,5 @@ import { + CellContext, ColumnDef, createColumnHelper, PaginationState, @@ -23,23 +24,23 @@ export const useHeadlessColumns = ({ const { t } = useCunningham(); const columnHelper = createColumnHelper(); let headlessColumns = columns.map((column) => { + // Based on types we can assume that at least one of both is defined. + const id = (column.id ?? column.field) as string; const opts = { - id: column.renderCell ? column.id : column.id ?? column.field, + id, enableSorting: column.enableSorting, header: column.headerName, + cell: (info: CellContext) => { + if (column.renderCell) { + return column.renderCell({ row: info.row.original }); + } + return info.cell.getValue(); + }, }; - if (!column.renderCell) { - // The any cast is needed because the type of the accessor is hard-defined on react-table. - // On our side we only use string as type for simplicity purpose. + if (column.field) { return columnHelper.accessor(column.field as any, opts); } - - return columnHelper.display({ - ...opts, - cell: (info) => { - return column.renderCell({ row: info.row.original }); - }, - }); + return columnHelper.display(opts); }); if (enableRowSelection) { headlessColumns = [