@carlonicora/nextjs-jsonapi 6.32.1 → 6.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BlockNoteEditor-XIEMUQCO.mjs → BlockNoteEditor-DCVT7YHE.mjs} +4 -4
- package/dist/{BlockNoteEditor-RD3JNEV5.js → BlockNoteEditor-ZVK5MD34.js} +24 -24
- package/dist/{BlockNoteEditor-RD3JNEV5.js.map → BlockNoteEditor-ZVK5MD34.js.map} +1 -1
- package/dist/{BlockNoteViewer-RQBYHBQL.js → BlockNoteViewer-2KPGZSTJ.js} +3 -3
- package/dist/{BlockNoteViewer-RQBYHBQL.js.map → BlockNoteViewer-2KPGZSTJ.js.map} +1 -1
- package/dist/{BlockNoteViewer-NVSR67X5.mjs → BlockNoteViewer-XDAC6AL7.mjs} +2 -2
- package/dist/billing/index.js +477 -469
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +15 -7
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-5H25LM2Z.js → chunk-576XHOYH.js} +5 -5
- package/dist/{chunk-5H25LM2Z.js.map → chunk-576XHOYH.js.map} +1 -1
- package/dist/{chunk-NPGX2GJD.js → chunk-CI64OTRX.js} +98 -75
- package/dist/chunk-CI64OTRX.js.map +1 -0
- package/dist/{chunk-AEBZ67JJ.mjs → chunk-ETU3AW2X.mjs} +2 -2
- package/dist/{chunk-4JVLLBRS.mjs → chunk-GUNH3THR.mjs} +24 -1
- package/dist/chunk-GUNH3THR.mjs.map +1 -0
- package/dist/{chunk-SQNFYMYE.js → chunk-KH4Y4FU4.js} +1532 -1288
- package/dist/chunk-KH4Y4FU4.js.map +1 -0
- package/dist/{chunk-PNCO3HMJ.js → chunk-TCAOU6NH.js} +3 -3
- package/dist/{chunk-PNCO3HMJ.js.map → chunk-TCAOU6NH.js.map} +1 -1
- package/dist/{chunk-F25BVBEH.mjs → chunk-V4NPMUZ2.mjs} +2663 -2419
- package/dist/chunk-V4NPMUZ2.mjs.map +1 -0
- package/dist/{chunk-AMQFKCAX.mjs → chunk-WPFEPIKP.mjs} +2 -2
- package/dist/client/index.js +6 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +5 -3
- package/dist/components/index.js +12 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +11 -3
- package/dist/components/tables/ContentListTable.d.ts +14 -0
- package/dist/components/tables/ContentListTable.d.ts.map +1 -1
- package/dist/components/tables/SortableHeader.d.ts +17 -0
- package/dist/components/tables/SortableHeader.d.ts.map +1 -0
- package/dist/components/tables/cells/cell.date.d.ts +4 -0
- package/dist/components/tables/cells/cell.date.d.ts.map +1 -1
- package/dist/components/tables/cells/cell.dateTime.d.ts +4 -0
- package/dist/components/tables/cells/cell.dateTime.d.ts.map +1 -1
- package/dist/components/tables/cells/cell.link.d.ts +4 -0
- package/dist/components/tables/cells/cell.link.d.ts.map +1 -1
- package/dist/components/tables/cells/cell.text.d.ts +4 -0
- package/dist/components/tables/cells/cell.text.d.ts.map +1 -1
- package/dist/components/tables/index.d.ts +2 -0
- package/dist/components/tables/index.d.ts.map +1 -1
- package/dist/components/tables/sorting.d.ts +24 -0
- package/dist/components/tables/sorting.d.ts.map +1 -0
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/email-automations/index.js +120 -115
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +18 -13
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/analytics/index.js +114 -114
- package/dist/features/analytics/index.mjs +3 -3
- package/dist/features/analytics/server.js +4 -4
- package/dist/features/analytics/server.mjs +2 -2
- package/dist/features/billing/stripe-coupon/components/lists/CouponCodesList.d.ts.map +1 -1
- package/dist/features/billing/stripe-coupon/components/lists/CouponsList.d.ts.map +1 -1
- package/dist/features/billing/stripe-coupon/data/stripe-coupon-code.service.d.ts +2 -0
- package/dist/features/billing/stripe-coupon/data/stripe-coupon-code.service.d.ts.map +1 -1
- package/dist/features/billing/stripe-coupon/data/stripe-coupon.service.d.ts +1 -0
- package/dist/features/billing/stripe-coupon/data/stripe-coupon.service.d.ts.map +1 -1
- package/dist/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.d.ts.map +1 -1
- package/dist/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.d.ts.map +1 -1
- package/dist/features/billing/stripe-price/components/lists/PricesList.d.ts.map +1 -1
- package/dist/features/billing/stripe-price/data/stripe-price.service.d.ts +2 -0
- package/dist/features/billing/stripe-price/data/stripe-price.service.d.ts.map +1 -1
- package/dist/features/billing/stripe-price/hooks/useStripePriceTableStructure.d.ts.map +1 -1
- package/dist/features/billing/stripe-product/components/lists/ProductsList.d.ts.map +1 -1
- package/dist/features/billing/stripe-product/data/stripe-product.service.d.ts +2 -0
- package/dist/features/billing/stripe-product/data/stripe-product.service.d.ts.map +1 -1
- package/dist/features/billing/stripe-product/hooks/useStripeProductTableStructure.d.ts.map +1 -1
- package/dist/features/blog/components/admin/BlogPostsList.d.ts.map +1 -1
- package/dist/features/blog/data/BlogPostService.d.ts +1 -0
- package/dist/features/blog/data/BlogPostService.d.ts.map +1 -1
- package/dist/features/blog/index.js +134 -132
- package/dist/features/blog/index.js.map +1 -1
- package/dist/features/blog/index.mjs +8 -6
- package/dist/features/blog/index.mjs.map +1 -1
- package/dist/features/blog/server.js +7 -7
- package/dist/features/blog/server.mjs +1 -1
- package/dist/features/company/components/lists/CompaniesList.d.ts.map +1 -1
- package/dist/features/company/data/company.service.d.ts +1 -0
- package/dist/features/company/data/company.service.d.ts.map +1 -1
- package/dist/features/content/data/content.service.d.ts +3 -0
- package/dist/features/content/data/content.service.d.ts.map +1 -1
- package/dist/features/email-automation/components/lists/EmailAutomationRecipientsList.d.ts.map +1 -1
- package/dist/features/email-automation/components/lists/EmailAutomationsList.d.ts.map +1 -1
- package/dist/features/email-automation/data/EmailAutomationService.d.ts +3 -0
- package/dist/features/email-automation/data/EmailAutomationService.d.ts.map +1 -1
- package/dist/features/email-automation/hooks/useEmailAutomationTableStructure.d.ts.map +1 -1
- package/dist/features/email-blast/components/lists/EmailBlastsList.d.ts.map +1 -1
- package/dist/features/email-blast/data/EmailBlastService.d.ts +1 -0
- package/dist/features/email-blast/data/EmailBlastService.d.ts.map +1 -1
- package/dist/features/email-blast/hooks/useEmailBlastTableStructure.d.ts.map +1 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/how-to/components/lists/HowToList.d.ts.map +1 -1
- package/dist/features/how-to/data/HowToService.d.ts +2 -0
- package/dist/features/how-to/data/HowToService.d.ts.map +1 -1
- package/dist/features/notification/components/lists/NotificationsList.d.ts.map +1 -1
- package/dist/features/notification/data/notification.service.d.ts +2 -0
- package/dist/features/notification/data/notification.service.d.ts.map +1 -1
- package/dist/features/referral/components/containers/ReferralsAdminContainer.d.ts.map +1 -1
- package/dist/features/referral/data/ReferralAdminService.d.ts +1 -0
- package/dist/features/referral/data/ReferralAdminService.d.ts.map +1 -1
- package/dist/features/referral/hooks/useReferralAdminReferralTableStructure.d.ts.map +1 -1
- package/dist/features/role/components/lists/RolesList.d.ts.map +1 -1
- package/dist/features/role/components/lists/UserRolesList.d.ts.map +1 -1
- package/dist/features/role/data/role.service.d.ts +3 -0
- package/dist/features/role/data/role.service.d.ts.map +1 -1
- package/dist/features/tokenusage/index.js +114 -114
- package/dist/features/tokenusage/index.mjs +3 -3
- package/dist/features/user/components/lists/AdminUsersList.d.ts.map +1 -1
- package/dist/features/user/components/lists/CompanyUsersList.d.ts.map +1 -1
- package/dist/features/user/components/lists/PlatformUsersList.d.ts.map +1 -1
- package/dist/features/user/components/lists/RoleUsersList.d.ts.map +1 -1
- package/dist/features/user/components/lists/UsersListByContentIds.d.ts.map +1 -1
- package/dist/features/user/data/user.service.d.ts +11 -0
- package/dist/features/user/data/user.service.d.ts.map +1 -1
- package/dist/features/waitlist/components/lists/WaitlistList.d.ts.map +1 -1
- package/dist/features/waitlist/data/WaitlistService.d.ts +2 -0
- package/dist/features/waitlist/data/WaitlistService.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useDataListRetriever.d.ts +13 -0
- package/dist/hooks/useDataListRetriever.d.ts.map +1 -1
- package/dist/hooks/useListSortParam.d.ts +23 -0
- package/dist/hooks/useListSortParam.d.ts.map +1 -0
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/dist/shadcnui/ui/calendar.d.ts +1 -1
- package/dist/shadcnui/ui/calendar.d.ts.map +1 -1
- package/messages/en.json +11 -0
- package/messages/it.json +11 -0
- package/package.json +1 -1
- package/src/components/tables/ContentListTable.tsx +108 -10
- package/src/components/tables/SortableHeader.tsx +43 -0
- package/src/components/tables/__tests__/ContentListTable.rendering.spec.tsx +1 -0
- package/src/components/tables/__tests__/ContentListTable.sort.spec.tsx +273 -0
- package/src/components/tables/__tests__/sorting.spec.ts +61 -0
- package/src/components/tables/cells/cell.date.tsx +10 -2
- package/src/components/tables/cells/cell.dateTime.tsx +10 -2
- package/src/components/tables/cells/cell.link.tsx +6 -1
- package/src/components/tables/cells/cell.text.tsx +11 -2
- package/src/components/tables/index.ts +2 -0
- package/src/components/tables/sorting.ts +41 -0
- package/src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx +2 -0
- package/src/features/billing/stripe-coupon/components/lists/CouponsList.tsx +2 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.service.ts +4 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +7 -1
- package/src/features/billing/stripe-coupon/hooks/__tests__/useStripeCouponCodeTableStructure.sort.spec.tsx +30 -0
- package/src/features/billing/stripe-coupon/hooks/__tests__/useStripeCouponTableStructure.sort.spec.tsx +33 -0
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +5 -4
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +7 -4
- package/src/features/billing/stripe-price/components/lists/PricesList.tsx +2 -0
- package/src/features/billing/stripe-price/data/stripe-price.service.ts +4 -0
- package/src/features/billing/stripe-price/hooks/__tests__/useStripePriceTableStructure.sort.spec.tsx +29 -0
- package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +8 -5
- package/src/features/billing/stripe-product/components/lists/ProductsList.tsx +2 -0
- package/src/features/billing/stripe-product/data/stripe-product.service.ts +4 -0
- package/src/features/billing/stripe-product/hooks/__tests__/useStripeProductTableStructure.sort.spec.tsx +29 -0
- package/src/features/billing/stripe-product/hooks/useStripeProductTableStructure.tsx +3 -2
- package/src/features/blog/components/admin/BlogPostsList.tsx +2 -0
- package/src/features/blog/data/BlogPostService.ts +3 -1
- package/src/features/blog/hooks/__tests__/useBlogPostTableStructure.sort.spec.tsx +19 -0
- package/src/features/blog/hooks/useBlogPostTableStructure.tsx +2 -2
- package/src/features/company/components/lists/CompaniesList.tsx +2 -0
- package/src/features/company/data/company.service.ts +3 -1
- package/src/features/company/hooks/__tests__/useCompanyTableStructure.sort.spec.tsx +19 -0
- package/src/features/company/hooks/useCompanyTableStructure.tsx +1 -1
- package/src/features/content/data/content.service.ts +12 -1
- package/src/features/content/hooks/__tests__/useContentTableStructure.sort.spec.tsx +21 -0
- package/src/features/content/hooks/useContentTableStructure.tsx +1 -1
- package/src/features/email-automation/components/lists/EmailAutomationRecipientsList.tsx +2 -0
- package/src/features/email-automation/components/lists/EmailAutomationsList.tsx +2 -0
- package/src/features/email-automation/data/EmailAutomationService.ts +9 -1
- package/src/features/email-automation/hooks/__tests__/useEmailAutomationEnrollmentTableStructure.sort.spec.tsx +21 -0
- package/src/features/email-automation/hooks/__tests__/useEmailAutomationTableStructure.sort.spec.tsx +29 -0
- package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +4 -4
- package/src/features/email-automation/hooks/useEmailAutomationTableStructure.tsx +5 -4
- package/src/features/email-blast/components/lists/EmailBlastsList.tsx +2 -0
- package/src/features/email-blast/data/EmailBlastService.ts +5 -1
- package/src/features/email-blast/hooks/__tests__/useEmailBlastTableStructure.sort.spec.tsx +29 -0
- package/src/features/email-blast/hooks/useEmailBlastTableStructure.tsx +16 -8
- package/src/features/how-to/components/lists/HowToList.tsx +2 -0
- package/src/features/how-to/data/HowToService.ts +4 -0
- package/src/features/how-to/hooks/__tests__/useHowToTableStructure.sort.spec.tsx +18 -0
- package/src/features/how-to/hooks/useHowToTableStructure.tsx +1 -1
- package/src/features/notification/components/lists/NotificationsList.tsx +2 -0
- package/src/features/notification/data/notification.service.ts +4 -0
- package/src/features/notification/hooks/__tests__/useNotificationTableStructure.sort.spec.tsx +19 -0
- package/src/features/notification/hooks/useNotificationTableStructure.tsx +1 -1
- package/src/features/referral/components/containers/ReferralsAdminContainer.tsx +4 -2
- package/src/features/referral/data/ReferralAdminService.ts +6 -1
- package/src/features/referral/hooks/__tests__/useReferralAdminReferralTableStructure.sort.spec.tsx +35 -0
- package/src/features/referral/hooks/useReferralAdminReferralTableStructure.tsx +10 -5
- package/src/features/role/components/lists/RolesList.tsx +2 -0
- package/src/features/role/components/lists/UserRolesList.tsx +2 -0
- package/src/features/role/data/role.service.ts +7 -1
- package/src/features/role/hooks/__tests__/useRoleTableStructure.sort.spec.tsx +16 -0
- package/src/features/role/hooks/useRoleTableStructure.tsx +2 -2
- package/src/features/user/components/lists/AdminUsersList.tsx +2 -0
- package/src/features/user/components/lists/CompanyUsersList.tsx +4 -0
- package/src/features/user/components/lists/PlatformUsersList.tsx +2 -0
- package/src/features/user/components/lists/RoleUsersList.tsx +2 -0
- package/src/features/user/components/lists/UsersListByContentIds.tsx +2 -0
- package/src/features/user/data/user.service.ts +28 -1
- package/src/features/user/hooks/__tests__/useUserTableStructure.sort.spec.tsx +16 -0
- package/src/features/user/hooks/useUserTableStructure.tsx +2 -2
- package/src/features/waitlist/components/lists/WaitlistList.tsx +2 -0
- package/src/features/waitlist/data/WaitlistService.ts +4 -0
- package/src/features/waitlist/hooks/__tests__/useWaitlistTableStructure.sort.spec.tsx +17 -0
- package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +3 -3
- package/src/hooks/__tests__/useDataListRetriever.sort.spec.tsx +114 -0
- package/src/hooks/__tests__/useListSortParam.spec.tsx +91 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useDataListRetriever.ts +44 -4
- package/src/hooks/useListSortParam.ts +53 -0
- package/src/shadcnui/ui/__tests__/calendar.spec.tsx +222 -0
- package/src/shadcnui/ui/calendar.tsx +28 -1
- package/dist/chunk-4JVLLBRS.mjs.map +0 -1
- package/dist/chunk-F25BVBEH.mjs.map +0 -1
- package/dist/chunk-NPGX2GJD.js.map +0 -1
- package/dist/chunk-SQNFYMYE.js.map +0 -1
- /package/dist/{BlockNoteEditor-XIEMUQCO.mjs.map → BlockNoteEditor-DCVT7YHE.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-NVSR67X5.mjs.map → BlockNoteViewer-XDAC6AL7.mjs.map} +0 -0
- /package/dist/{chunk-AEBZ67JJ.mjs.map → chunk-ETU3AW2X.mjs.map} +0 -0
- /package/dist/{chunk-AMQFKCAX.mjs.map → chunk-WPFEPIKP.mjs.map} +0 -0
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import "../../client";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ExpandedState,
|
|
6
|
+
flexRender,
|
|
7
|
+
getCoreRowModel,
|
|
8
|
+
getExpandedRowModel,
|
|
9
|
+
getSortedRowModel,
|
|
10
|
+
SortingState,
|
|
11
|
+
TableState,
|
|
12
|
+
useReactTable,
|
|
13
|
+
} from "@tanstack/react-table";
|
|
5
14
|
|
|
6
15
|
import { cn } from "@/index";
|
|
7
16
|
import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react";
|
|
8
17
|
import { useTranslations } from "next-intl";
|
|
9
18
|
import React, { ReactNode, memo, useMemo, useState } from "react";
|
|
10
|
-
import { DataListRetriever, TableContent, useTableGenerator } from "../../hooks";
|
|
19
|
+
import { DataListRetriever, TableContent, useListSortParam, useTableGenerator } from "../../hooks";
|
|
11
20
|
import { ModuleWithPermissions } from "../../permissions";
|
|
12
21
|
import { Button, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "../../shadcnui";
|
|
13
22
|
import { useIsMobile } from "../../utils/use-mobile";
|
|
14
23
|
import { MicroLabel } from "../typography";
|
|
15
24
|
import { ContentTableSearch } from "./ContentTableSearch";
|
|
25
|
+
import { SortableHeader } from "./SortableHeader";
|
|
26
|
+
import { nextSortValue, sortFieldsOf, sortStateFor } from "./sorting";
|
|
27
|
+
|
|
28
|
+
/** Column `meta` read by the table: cell classes and, for a sortable column, the fields it sorts by. */
|
|
29
|
+
type ColumnMeta = { className?: string; sortFields?: string[] };
|
|
30
|
+
|
|
31
|
+
const ARIA_SORT = { asc: "ascending", desc: "descending" } as const;
|
|
16
32
|
|
|
17
33
|
const EMPTY_ARRAY: any[] = [];
|
|
18
34
|
|
|
@@ -76,6 +92,20 @@ type ContentListTableProps = {
|
|
|
76
92
|
* toggles it via `row.getToggleExpandedHandler()`.
|
|
77
93
|
*/
|
|
78
94
|
renderExpandedRow?: (rowData: any) => ReactNode;
|
|
95
|
+
/**
|
|
96
|
+
* Where a sortable column (`enableSorting: true`) sorts. `"server"` (default): a
|
|
97
|
+
* header click sets the retriever's sort and the API orders the whole list.
|
|
98
|
+
* `"client"`: for small, fully loaded tables only; the loaded rows are sorted in
|
|
99
|
+
* the browser and the sort is kept in the page address under `sortKey`.
|
|
100
|
+
*/
|
|
101
|
+
sortMode?: "server" | "client";
|
|
102
|
+
/** Address key of the sort in `sortMode="client"` (`sort[<sortKey>]`). Required in client mode. */
|
|
103
|
+
sortKey?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Client mode only: sorts the loaded items itself (grouped tables that sort inside
|
|
106
|
+
* their groups). Without it, rows are sorted by the column's accessor.
|
|
107
|
+
*/
|
|
108
|
+
clientSort?: <T>(rows: T[], sort: string | undefined) => T[];
|
|
79
109
|
};
|
|
80
110
|
|
|
81
111
|
type CellRenderer = (context: unknown) => ReactNode;
|
|
@@ -123,8 +153,19 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
123
153
|
if (data.isLoaded) setPendingDirection(null);
|
|
124
154
|
}, [data.isLoaded]);
|
|
125
155
|
|
|
156
|
+
const isClientSort = props.sortMode === "client";
|
|
157
|
+
const clientSortParam = useListSortParam({ sortKey: props.sortKey ?? "" });
|
|
158
|
+
const activeSort = isClientSort ? clientSortParam.sort : data.sort;
|
|
159
|
+
const { clientSort } = props;
|
|
160
|
+
|
|
161
|
+
const sourceData = data?.data ?? EMPTY_ARRAY;
|
|
162
|
+
const rowsToRender = useMemo(
|
|
163
|
+
() => (isClientSort && clientSort ? clientSort(sourceData, activeSort) : sourceData),
|
|
164
|
+
[isClientSort, clientSort, sourceData, activeSort],
|
|
165
|
+
);
|
|
166
|
+
|
|
126
167
|
const { data: tableData, columns: tableColumns } = useTableGenerator(props.tableGeneratorType, {
|
|
127
|
-
data:
|
|
168
|
+
data: rowsToRender,
|
|
128
169
|
fields: fields,
|
|
129
170
|
checkedIds: checkedIds,
|
|
130
171
|
toggleId: toggleId,
|
|
@@ -144,10 +185,33 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
144
185
|
[fields],
|
|
145
186
|
);
|
|
146
187
|
|
|
188
|
+
// Client mode without `clientSort`: TanStack sorts the loaded rows by the column the
|
|
189
|
+
// address sort names. Server mode never sorts rows: the retriever owns the order.
|
|
190
|
+
const clientSorting = useMemo((): SortingState | undefined => {
|
|
191
|
+
if (!isClientSort || clientSort || !activeSort) return undefined;
|
|
192
|
+
for (const column of tableColumns as Array<{ id?: string; accessorKey?: string; meta?: unknown }>) {
|
|
193
|
+
const id = column.id ?? column.accessorKey;
|
|
194
|
+
if (!id) continue;
|
|
195
|
+
const direction = sortStateFor(activeSort, sortFieldsOf({ id, columnDef: { meta: column.meta } }));
|
|
196
|
+
if (direction) return [{ id, desc: direction === "desc" }];
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}, [isClientSort, clientSort, activeSort, tableColumns]);
|
|
200
|
+
|
|
201
|
+
const expansionEnabled = !!props.renderExpandedRow || !!props.expandable;
|
|
202
|
+
const tableState: Partial<TableState> | undefined =
|
|
203
|
+
expansionEnabled || isClientSort
|
|
204
|
+
? {
|
|
205
|
+
...(expansionEnabled && { expanded }),
|
|
206
|
+
...(isClientSort && { sorting: clientSorting ?? [] }),
|
|
207
|
+
}
|
|
208
|
+
: undefined;
|
|
209
|
+
|
|
147
210
|
const table = useReactTable({
|
|
148
211
|
data: tableData,
|
|
149
212
|
columns: tableColumns,
|
|
150
213
|
getCoreRowModel: getCoreRowModel(),
|
|
214
|
+
...(isClientSort ? { getSortedRowModel: getSortedRowModel(), manualSorting: false } : { manualSorting: true }),
|
|
151
215
|
// Pagination is server-side (the data retriever), never TanStack's. Left on,
|
|
152
216
|
// every core-row-model rebuild queues resetPageIndex -> setState -> re-render,
|
|
153
217
|
// and the table generators rebuild their columns on every render, so it never
|
|
@@ -160,14 +224,13 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
160
224
|
getRowId: (row: TableContent<any>, index: number) => row.jsonApiData?.id ?? String(index),
|
|
161
225
|
getRowCanExpand: () => true,
|
|
162
226
|
onExpandedChange: setExpanded,
|
|
163
|
-
state: { expanded },
|
|
164
227
|
}),
|
|
165
228
|
...(props.expandable && {
|
|
166
229
|
getExpandedRowModel: getExpandedRowModel(),
|
|
167
230
|
getSubRows: props.getSubRows,
|
|
168
231
|
onExpandedChange: setExpanded,
|
|
169
|
-
state: { expanded },
|
|
170
232
|
}),
|
|
233
|
+
...(tableState && { state: tableState }),
|
|
171
234
|
initialState: {
|
|
172
235
|
columnVisibility,
|
|
173
236
|
},
|
|
@@ -291,10 +354,45 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
291
354
|
table.getHeaderGroups().map((headerGroup) => (
|
|
292
355
|
<TableRow key={headerGroup.id}>
|
|
293
356
|
{headerGroup.headers.map((header) => {
|
|
294
|
-
const meta = header.column.columnDef.meta as
|
|
357
|
+
const meta = header.column.columnDef.meta as ColumnMeta | undefined;
|
|
358
|
+
const label = header.isPlaceholder
|
|
359
|
+
? null
|
|
360
|
+
: flexRender(header.column.columnDef.header, header.getContext());
|
|
361
|
+
// Only an explicit `true` opts a column in: TanStack defaults a missing key to sortable.
|
|
362
|
+
// A server-mode retriever without `setSort` (hand-built) cannot sort: plain header.
|
|
363
|
+
if (
|
|
364
|
+
header.isPlaceholder ||
|
|
365
|
+
header.column.columnDef.enableSorting !== true ||
|
|
366
|
+
(!isClientSort && !data.setSort)
|
|
367
|
+
) {
|
|
368
|
+
return (
|
|
369
|
+
<TableHead key={header.id} className={meta?.className}>
|
|
370
|
+
{label}
|
|
371
|
+
</TableHead>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const sortFields = sortFieldsOf(header.column);
|
|
375
|
+
const direction = sortStateFor(activeSort, sortFields);
|
|
376
|
+
const defaultDirection =
|
|
377
|
+
!isClientSort && !activeSort ? sortStateFor(data.defaultSort, sortFields) : undefined;
|
|
378
|
+
const onToggle = () => {
|
|
379
|
+
const next = nextSortValue(activeSort, sortFields);
|
|
380
|
+
if (isClientSort) clientSortParam.setSort(next);
|
|
381
|
+
else data.setSort?.(next);
|
|
382
|
+
};
|
|
295
383
|
return (
|
|
296
|
-
<TableHead
|
|
297
|
-
{header.
|
|
384
|
+
<TableHead
|
|
385
|
+
key={header.id}
|
|
386
|
+
className={meta?.className}
|
|
387
|
+
aria-sort={direction ? ARIA_SORT[direction] : "none"}
|
|
388
|
+
>
|
|
389
|
+
<SortableHeader
|
|
390
|
+
columnId={header.column.id}
|
|
391
|
+
label={label}
|
|
392
|
+
direction={direction}
|
|
393
|
+
defaultDirection={defaultDirection}
|
|
394
|
+
onToggle={onToggle}
|
|
395
|
+
/>
|
|
298
396
|
</TableHead>
|
|
299
397
|
);
|
|
300
398
|
})}
|
|
@@ -318,7 +416,7 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
318
416
|
className={`group ${onRowClick ? "hover:bg-muted/50 cursor-pointer" : ""}`}
|
|
319
417
|
>
|
|
320
418
|
{row.getVisibleCells().map((cell) => {
|
|
321
|
-
const meta = cell.column.columnDef.meta as
|
|
419
|
+
const meta = cell.column.columnDef.meta as ColumnMeta | undefined;
|
|
322
420
|
return (
|
|
323
421
|
<TableCell key={cell.id} className={meta?.className}>
|
|
324
422
|
{renderCell(cell.column.columnDef.cell, cell.getContext())}
|
|
@@ -345,7 +443,7 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
345
443
|
className={`group ${onRowClick ? "hover:bg-muted/50 cursor-pointer" : ""}`}
|
|
346
444
|
>
|
|
347
445
|
{row.getVisibleCells().map((cell) => {
|
|
348
|
-
const meta = cell.column.columnDef.meta as
|
|
446
|
+
const meta = cell.column.columnDef.meta as ColumnMeta | undefined;
|
|
349
447
|
return (
|
|
350
448
|
<TableCell key={cell.id} className={meta?.className}>
|
|
351
449
|
{renderCell(cell.column.columnDef.cell, cell.getContext())}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
|
|
4
|
+
import { ReactNode } from "react";
|
|
5
|
+
import { Button } from "../../shadcnui";
|
|
6
|
+
import { SortDirection } from "./sorting";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The label of a sortable column header: a button that toggles the column's sort.
|
|
10
|
+
*
|
|
11
|
+
* `direction` is the sort the user chose (primary colour). `defaultDirection` is
|
|
12
|
+
* the list's display-only default order, shown as a muted arrow while no sort is
|
|
13
|
+
* chosen. The enclosing `<TableHead>` carries `aria-sort`.
|
|
14
|
+
*/
|
|
15
|
+
export function SortableHeader(props: {
|
|
16
|
+
columnId: string;
|
|
17
|
+
label: ReactNode;
|
|
18
|
+
direction?: SortDirection;
|
|
19
|
+
defaultDirection?: SortDirection;
|
|
20
|
+
onToggle: () => void;
|
|
21
|
+
}) {
|
|
22
|
+
const shown = props.direction ?? props.defaultDirection;
|
|
23
|
+
const Icon = shown === "asc" ? ArrowUp : shown === "desc" ? ArrowDown : ArrowUpDown;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Button
|
|
27
|
+
type="button"
|
|
28
|
+
variant="ghost"
|
|
29
|
+
size="xs"
|
|
30
|
+
className={props.direction ? "text-primary" : "text-muted-foreground"}
|
|
31
|
+
data-testid={`list-sort-header-${props.columnId}`}
|
|
32
|
+
onClick={props.onToggle}
|
|
33
|
+
>
|
|
34
|
+
{props.label}
|
|
35
|
+
<Icon
|
|
36
|
+
data-testid="list-sort-icon"
|
|
37
|
+
data-direction={shown ?? "none"}
|
|
38
|
+
className={props.direction ? undefined : "text-muted-foreground"}
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
/>
|
|
41
|
+
</Button>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ContentListTable } from "../ContentListTable";
|
|
4
|
+
|
|
5
|
+
// The columns the mocked table generator returns. Each test sets them; the
|
|
6
|
+
// generator maps every data item to a row that also carries `jsonApiData`, as
|
|
7
|
+
// the sibling ContentListTable specs do.
|
|
8
|
+
const mockColumns = vi.hoisted(() => ({ columns: [] as any[] }));
|
|
9
|
+
|
|
10
|
+
vi.mock("../../../hooks", async () => {
|
|
11
|
+
const actual = await vi.importActual("../../../hooks");
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
useTableGenerator: vi.fn((_type: any, params: any) => ({
|
|
15
|
+
data: params.data.map((item: any) => ({ ...item, jsonApiData: item })),
|
|
16
|
+
columns: mockColumns.columns,
|
|
17
|
+
})),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
vi.mock("../ContentTableSearch", () => ({
|
|
22
|
+
ContentTableSearch: () => <div data-testid="content-table-search">Search</div>,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const nameColumn = (enableSorting?: boolean) => ({
|
|
26
|
+
id: "name",
|
|
27
|
+
header: "Name",
|
|
28
|
+
accessorKey: "name",
|
|
29
|
+
cell: ({ row }: any) => row.original.name,
|
|
30
|
+
...(enableSorting !== undefined && { enableSorting }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const updatedAtColumn = {
|
|
34
|
+
id: "updatedAt",
|
|
35
|
+
header: "Updated",
|
|
36
|
+
accessorKey: "updatedAt",
|
|
37
|
+
cell: ({ row }: any) => row.original.updatedAt,
|
|
38
|
+
enableSorting: true,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const rows = [
|
|
42
|
+
{ id: "1", name: "Bravo", updatedAt: "2" },
|
|
43
|
+
{ id: "2", name: "Alpha", updatedAt: "3" },
|
|
44
|
+
{ id: "3", name: "Charlie", updatedAt: "1" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const retriever = (overrides: Record<string, any> = {}) =>
|
|
48
|
+
({
|
|
49
|
+
data: rows,
|
|
50
|
+
isLoaded: true,
|
|
51
|
+
ready: true,
|
|
52
|
+
next: undefined,
|
|
53
|
+
previous: undefined,
|
|
54
|
+
refresh: vi.fn(),
|
|
55
|
+
addAdditionalParameter: vi.fn(),
|
|
56
|
+
removeAdditionalParameter: vi.fn(),
|
|
57
|
+
setRefreshedElement: vi.fn(),
|
|
58
|
+
removeElement: vi.fn(),
|
|
59
|
+
search: vi.fn(),
|
|
60
|
+
setReady: vi.fn(),
|
|
61
|
+
setSort: vi.fn(),
|
|
62
|
+
isSearch: false,
|
|
63
|
+
...overrides,
|
|
64
|
+
}) as any;
|
|
65
|
+
|
|
66
|
+
const mockModule = { name: "things", icon: undefined } as any;
|
|
67
|
+
|
|
68
|
+
const renderedNames = () =>
|
|
69
|
+
screen
|
|
70
|
+
.getAllByRole("row")
|
|
71
|
+
.slice(1)
|
|
72
|
+
.map((row) => row.textContent);
|
|
73
|
+
|
|
74
|
+
const headCell = (columnId: string) => screen.getByTestId(`list-sort-header-${columnId}`).closest("th")!;
|
|
75
|
+
|
|
76
|
+
describe("ContentListTable sortable headers", () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
window.history.replaceState(null, "", "/things");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
window.history.replaceState(null, "", "/");
|
|
83
|
+
vi.restoreAllMocks();
|
|
84
|
+
mockColumns.columns = [];
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("renders a sort button only for a column with enableSorting: true", () => {
|
|
88
|
+
mockColumns.columns = [nameColumn(true)];
|
|
89
|
+
render(<ContentListTable data={retriever()} fields={["name"]} tableGeneratorType={mockModule} />);
|
|
90
|
+
expect(screen.getByTestId("list-sort-header-name")).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("renders no sort button when the column omits enableSorting", () => {
|
|
94
|
+
mockColumns.columns = [nameColumn()];
|
|
95
|
+
render(<ContentListTable data={retriever()} fields={["name"]} tableGeneratorType={mockModule} />);
|
|
96
|
+
expect(screen.queryByTestId("list-sort-header-name")).toBeNull();
|
|
97
|
+
expect(screen.getByText("Name").closest("th")).not.toHaveAttribute("aria-sort");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("renders a plain header in server mode when a hand-built retriever has no setSort", () => {
|
|
101
|
+
mockColumns.columns = [nameColumn(true)];
|
|
102
|
+
render(
|
|
103
|
+
<ContentListTable data={retriever({ setSort: undefined })} fields={["name"]} tableGeneratorType={mockModule} />,
|
|
104
|
+
);
|
|
105
|
+
expect(screen.queryByTestId("list-sort-header-name")).toBeNull();
|
|
106
|
+
expect(screen.getByText("Name").closest("th")).not.toHaveAttribute("aria-sort");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("still sorts in client mode when the retriever has no setSort", () => {
|
|
110
|
+
mockColumns.columns = [nameColumn(true)];
|
|
111
|
+
render(
|
|
112
|
+
<ContentListTable
|
|
113
|
+
data={retriever({ setSort: undefined })}
|
|
114
|
+
fields={["name"]}
|
|
115
|
+
tableGeneratorType={mockModule}
|
|
116
|
+
sortMode="client"
|
|
117
|
+
sortKey="things"
|
|
118
|
+
/>,
|
|
119
|
+
);
|
|
120
|
+
fireEvent.click(screen.getByTestId("list-sort-header-name"));
|
|
121
|
+
expect(renderedNames()).toEqual(["Alpha", "Bravo", "Charlie"]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("renders no sort button when enableSorting is false", () => {
|
|
125
|
+
mockColumns.columns = [nameColumn(false)];
|
|
126
|
+
render(<ContentListTable data={retriever()} fields={["name"]} tableGeneratorType={mockModule} />);
|
|
127
|
+
expect(screen.queryByTestId("list-sort-header-name")).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("cycles none -> asc -> desc -> none through data.setSort, with aria-sort following", () => {
|
|
131
|
+
mockColumns.columns = [nameColumn(true)];
|
|
132
|
+
const setSort = vi.fn();
|
|
133
|
+
|
|
134
|
+
const { rerender } = render(
|
|
135
|
+
<ContentListTable data={retriever({ setSort })} fields={["name"]} tableGeneratorType={mockModule} />,
|
|
136
|
+
);
|
|
137
|
+
expect(headCell("name")).toHaveAttribute("aria-sort", "none");
|
|
138
|
+
fireEvent.click(screen.getByTestId("list-sort-header-name"));
|
|
139
|
+
expect(setSort).toHaveBeenLastCalledWith("name");
|
|
140
|
+
|
|
141
|
+
rerender(
|
|
142
|
+
<ContentListTable
|
|
143
|
+
data={retriever({ setSort, sort: "name" })}
|
|
144
|
+
fields={["name"]}
|
|
145
|
+
tableGeneratorType={mockModule}
|
|
146
|
+
/>,
|
|
147
|
+
);
|
|
148
|
+
expect(headCell("name")).toHaveAttribute("aria-sort", "ascending");
|
|
149
|
+
fireEvent.click(screen.getByTestId("list-sort-header-name"));
|
|
150
|
+
expect(setSort).toHaveBeenLastCalledWith("-name");
|
|
151
|
+
|
|
152
|
+
rerender(
|
|
153
|
+
<ContentListTable
|
|
154
|
+
data={retriever({ setSort, sort: "-name" })}
|
|
155
|
+
fields={["name"]}
|
|
156
|
+
tableGeneratorType={mockModule}
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
expect(headCell("name")).toHaveAttribute("aria-sort", "descending");
|
|
160
|
+
fireEvent.click(screen.getByTestId("list-sort-header-name"));
|
|
161
|
+
expect(setSort).toHaveBeenLastCalledWith(null);
|
|
162
|
+
expect(setSort).toHaveBeenCalledTimes(3);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("does not reorder rows in server mode", () => {
|
|
166
|
+
mockColumns.columns = [nameColumn(true)];
|
|
167
|
+
render(<ContentListTable data={retriever({ sort: "name" })} fields={["name"]} tableGeneratorType={mockModule} />);
|
|
168
|
+
expect(renderedNames()).toEqual(["Bravo", "Alpha", "Charlie"]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("shows the defaultSort column with a muted arrow and aria-sort none", () => {
|
|
172
|
+
mockColumns.columns = [nameColumn(true), updatedAtColumn];
|
|
173
|
+
render(
|
|
174
|
+
<ContentListTable
|
|
175
|
+
data={retriever({ defaultSort: "-updatedAt" })}
|
|
176
|
+
fields={["name", "updatedAt"]}
|
|
177
|
+
tableGeneratorType={mockModule}
|
|
178
|
+
/>,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(headCell("updatedAt")).toHaveAttribute("aria-sort", "none");
|
|
182
|
+
const icon = within(screen.getByTestId("list-sort-header-updatedAt")).getByTestId("list-sort-icon");
|
|
183
|
+
expect(icon).toHaveAttribute("data-direction", "desc");
|
|
184
|
+
expect(icon.getAttribute("class")).toContain("text-muted-foreground");
|
|
185
|
+
expect(screen.getByTestId("list-sort-header-updatedAt").className).toContain("text-muted-foreground");
|
|
186
|
+
|
|
187
|
+
expect(within(screen.getByTestId("list-sort-header-name")).getByTestId("list-sort-icon")).toHaveAttribute(
|
|
188
|
+
"data-direction",
|
|
189
|
+
"none",
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("drops the muted default arrow once a sort is chosen", () => {
|
|
194
|
+
mockColumns.columns = [nameColumn(true), updatedAtColumn];
|
|
195
|
+
render(
|
|
196
|
+
<ContentListTable
|
|
197
|
+
data={retriever({ defaultSort: "-updatedAt", sort: "name" })}
|
|
198
|
+
fields={["name", "updatedAt"]}
|
|
199
|
+
tableGeneratorType={mockModule}
|
|
200
|
+
/>,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(within(screen.getByTestId("list-sort-header-updatedAt")).getByTestId("list-sort-icon")).toHaveAttribute(
|
|
204
|
+
"data-direction",
|
|
205
|
+
"none",
|
|
206
|
+
);
|
|
207
|
+
expect(screen.getByTestId("list-sort-header-name").className).toContain("text-primary");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("sorts the loaded rows in the browser in client mode, without calling setSort", () => {
|
|
211
|
+
mockColumns.columns = [nameColumn(true)];
|
|
212
|
+
const setSort = vi.fn();
|
|
213
|
+
render(
|
|
214
|
+
<ContentListTable
|
|
215
|
+
data={retriever({ setSort })}
|
|
216
|
+
fields={["name"]}
|
|
217
|
+
tableGeneratorType={mockModule}
|
|
218
|
+
sortMode="client"
|
|
219
|
+
sortKey="things"
|
|
220
|
+
/>,
|
|
221
|
+
);
|
|
222
|
+
expect(renderedNames()).toEqual(["Bravo", "Alpha", "Charlie"]);
|
|
223
|
+
|
|
224
|
+
fireEvent.click(screen.getByTestId("list-sort-header-name"));
|
|
225
|
+
expect(renderedNames()).toEqual(["Alpha", "Bravo", "Charlie"]);
|
|
226
|
+
expect(headCell("name")).toHaveAttribute("aria-sort", "ascending");
|
|
227
|
+
expect(new URLSearchParams(window.location.search).get("sort[things]")).toBe("name");
|
|
228
|
+
|
|
229
|
+
fireEvent.click(screen.getByTestId("list-sort-header-name"));
|
|
230
|
+
expect(renderedNames()).toEqual(["Charlie", "Bravo", "Alpha"]);
|
|
231
|
+
expect(headCell("name")).toHaveAttribute("aria-sort", "descending");
|
|
232
|
+
|
|
233
|
+
fireEvent.click(screen.getByTestId("list-sort-header-name"));
|
|
234
|
+
expect(renderedNames()).toEqual(["Bravo", "Alpha", "Charlie"]);
|
|
235
|
+
expect(headCell("name")).toHaveAttribute("aria-sort", "none");
|
|
236
|
+
|
|
237
|
+
expect(setSort).not.toHaveBeenCalled();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("restores a client sort from the address on mount", () => {
|
|
241
|
+
window.history.replaceState(null, "", "/things?sort%5Bthings%5D=-name");
|
|
242
|
+
mockColumns.columns = [nameColumn(true)];
|
|
243
|
+
render(
|
|
244
|
+
<ContentListTable
|
|
245
|
+
data={retriever()}
|
|
246
|
+
fields={["name"]}
|
|
247
|
+
tableGeneratorType={mockModule}
|
|
248
|
+
sortMode="client"
|
|
249
|
+
sortKey="things"
|
|
250
|
+
/>,
|
|
251
|
+
);
|
|
252
|
+
expect(renderedNames()).toEqual(["Charlie", "Bravo", "Alpha"]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("renders the order clientSort returns, called with (rows, sort)", () => {
|
|
256
|
+
window.history.replaceState(null, "", "/things?sort%5Bthings%5D=name");
|
|
257
|
+
mockColumns.columns = [nameColumn(true)];
|
|
258
|
+
const clientSort = vi.fn((items: any[]) => [items[2], items[0], items[1]]);
|
|
259
|
+
render(
|
|
260
|
+
<ContentListTable
|
|
261
|
+
data={retriever()}
|
|
262
|
+
fields={["name"]}
|
|
263
|
+
tableGeneratorType={mockModule}
|
|
264
|
+
sortMode="client"
|
|
265
|
+
sortKey="things"
|
|
266
|
+
clientSort={clientSort}
|
|
267
|
+
/>,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(clientSort).toHaveBeenLastCalledWith(rows, "name");
|
|
271
|
+
expect(renderedNames()).toEqual(["Charlie", "Bravo", "Alpha"]);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { nextSortValue, sortFieldsOf, sortStateFor } from "../sorting";
|
|
3
|
+
|
|
4
|
+
describe("nextSortValue", () => {
|
|
5
|
+
it("starts ascending when the column is not sorted", () => {
|
|
6
|
+
expect(nextSortValue(undefined, ["name"])).toBe("name");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("goes from ascending to descending", () => {
|
|
10
|
+
expect(nextSortValue("name", ["name"])).toBe("-name");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("clears the sort after descending", () => {
|
|
14
|
+
expect(nextSortValue("-name", ["name"])).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("starts ascending when another column holds the sort", () => {
|
|
18
|
+
expect(nextSortValue("-createdAt", ["name"])).toBe("name");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("writes every field of a composite column", () => {
|
|
22
|
+
expect(nextSortValue(undefined, ["lastName", "firstName"])).toBe("lastName,firstName");
|
|
23
|
+
expect(nextSortValue("lastName,firstName", ["lastName", "firstName"])).toBe("-lastName,-firstName");
|
|
24
|
+
expect(nextSortValue("-lastName,-firstName", ["lastName", "firstName"])).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("sortFieldsOf", () => {
|
|
29
|
+
it("uses meta.sortFields when the column declares them", () => {
|
|
30
|
+
expect(sortFieldsOf({ id: "name", columnDef: { meta: { sortFields: ["lastName", "firstName"] } } })).toEqual([
|
|
31
|
+
"lastName",
|
|
32
|
+
"firstName",
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("falls back to the column id", () => {
|
|
37
|
+
expect(sortFieldsOf({ id: "name", columnDef: { meta: { className: "w-10" } } })).toEqual(["name"]);
|
|
38
|
+
expect(sortFieldsOf({ id: "name", columnDef: {} })).toEqual(["name"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns no field for a column without id or sortFields", () => {
|
|
42
|
+
expect(sortFieldsOf({ columnDef: {} })).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("sortStateFor", () => {
|
|
47
|
+
it("reads a descending composite sort", () => {
|
|
48
|
+
expect(sortStateFor("-lastName,-firstName", ["lastName", "firstName"])).toBe("desc");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("reads an ascending sort", () => {
|
|
52
|
+
expect(sortStateFor("name", ["name"])).toBe("asc");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns undefined when the sort belongs to another column or is absent", () => {
|
|
56
|
+
expect(sortStateFor("-createdAt", ["name"])).toBeUndefined();
|
|
57
|
+
expect(sortStateFor(undefined, ["name"])).toBeUndefined();
|
|
58
|
+
expect(sortStateFor("lastName", ["lastName", "firstName"])).toBeUndefined();
|
|
59
|
+
expect(sortStateFor("name", [])).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -6,7 +6,14 @@ function DateCell({ date }: { date: Date }) {
|
|
|
6
6
|
return <span className="text-muted-foreground text-xs">{format(date, "dateMedium")}</span>;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export const cellDate = (params: {
|
|
9
|
+
export const cellDate = (params: {
|
|
10
|
+
name: string;
|
|
11
|
+
title: string;
|
|
12
|
+
/** Opt-in: the header sorts the list by this column. */
|
|
13
|
+
sortable?: boolean;
|
|
14
|
+
/** Stored fields the column sorts by, when they differ from `name`. */
|
|
15
|
+
sortFields?: string[];
|
|
16
|
+
}): ColumnDef<any> => {
|
|
10
17
|
return {
|
|
11
18
|
id: params.name,
|
|
12
19
|
accessorKey: params.name,
|
|
@@ -16,7 +23,8 @@ export const cellDate = (params: { name: string; title: string }): ColumnDef<any
|
|
|
16
23
|
if (!date) return null;
|
|
17
24
|
return <DateCell date={date} />;
|
|
18
25
|
},
|
|
19
|
-
enableSorting:
|
|
26
|
+
enableSorting: params.sortable === true,
|
|
27
|
+
...(params.sortFields && { meta: { sortFields: params.sortFields } }),
|
|
20
28
|
enableHiding: false,
|
|
21
29
|
};
|
|
22
30
|
};
|
|
@@ -6,7 +6,14 @@ function DateTimeCell({ date }: { date: Date }) {
|
|
|
6
6
|
return <span className="text-muted-foreground text-xs">{format(date, "dateTimeMedium")}</span>;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export const cellDateTime = (params: {
|
|
9
|
+
export const cellDateTime = (params: {
|
|
10
|
+
name: string;
|
|
11
|
+
title: string;
|
|
12
|
+
/** Opt-in: the header sorts the list by this column. */
|
|
13
|
+
sortable?: boolean;
|
|
14
|
+
/** Stored fields the column sorts by, when they differ from `name`. */
|
|
15
|
+
sortFields?: string[];
|
|
16
|
+
}): ColumnDef<any> => {
|
|
10
17
|
return {
|
|
11
18
|
id: params.name,
|
|
12
19
|
accessorKey: params.name,
|
|
@@ -16,7 +23,8 @@ export const cellDateTime = (params: { name: string; title: string }): ColumnDef
|
|
|
16
23
|
if (!date) return null;
|
|
17
24
|
return <DateTimeCell date={date} />;
|
|
18
25
|
},
|
|
19
|
-
enableSorting:
|
|
26
|
+
enableSorting: params.sortable === true,
|
|
27
|
+
...(params.sortFields && { meta: { sortFields: params.sortFields } }),
|
|
20
28
|
enableHiding: false,
|
|
21
29
|
};
|
|
22
30
|
};
|
|
@@ -6,13 +6,18 @@ export const cellLink = <T,>(params: {
|
|
|
6
6
|
name: string;
|
|
7
7
|
title: string;
|
|
8
8
|
generateUrl: (id: string) => string;
|
|
9
|
+
/** Opt-in: the header sorts the list by this column. */
|
|
10
|
+
sortable?: boolean;
|
|
11
|
+
/** Stored fields the column sorts by, when they differ from `name`. */
|
|
12
|
+
sortFields?: string[];
|
|
9
13
|
}): ColumnDef<T> => {
|
|
10
14
|
return {
|
|
11
15
|
id: params.name,
|
|
12
16
|
accessorKey: params.name,
|
|
13
17
|
header: params.title,
|
|
14
18
|
cell: ({ row }) => <Link href={params.generateUrl(row.getValue(params.id))}>{row.getValue(params.name)}</Link>,
|
|
15
|
-
enableSorting:
|
|
19
|
+
enableSorting: params.sortable === true,
|
|
20
|
+
...(params.sortFields && { meta: { sortFields: params.sortFields } }),
|
|
16
21
|
enableHiding: false,
|
|
17
22
|
};
|
|
18
23
|
};
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { ColumnDef } from "@tanstack/react-table";
|
|
2
2
|
|
|
3
|
-
export const cellText = (params: {
|
|
3
|
+
export const cellText = (params: {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
title: string;
|
|
7
|
+
/** Opt-in: the header sorts the list by this column. */
|
|
8
|
+
sortable?: boolean;
|
|
9
|
+
/** Stored fields the column sorts by, when they differ from `name`. */
|
|
10
|
+
sortFields?: string[];
|
|
11
|
+
}): ColumnDef<any> => {
|
|
4
12
|
return {
|
|
5
13
|
id: params.name,
|
|
6
14
|
accessorKey: params.name,
|
|
7
15
|
header: params.title,
|
|
8
16
|
cell: ({ row }) => row.getValue(params.name),
|
|
9
|
-
enableSorting:
|
|
17
|
+
enableSorting: params.sortable === true,
|
|
18
|
+
...(params.sortFields && { meta: { sortFields: params.sortFields } }),
|
|
10
19
|
enableHiding: false,
|
|
11
20
|
};
|
|
12
21
|
};
|