@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.
Files changed (233) hide show
  1. package/dist/{BlockNoteEditor-XIEMUQCO.mjs → BlockNoteEditor-DCVT7YHE.mjs} +4 -4
  2. package/dist/{BlockNoteEditor-RD3JNEV5.js → BlockNoteEditor-ZVK5MD34.js} +24 -24
  3. package/dist/{BlockNoteEditor-RD3JNEV5.js.map → BlockNoteEditor-ZVK5MD34.js.map} +1 -1
  4. package/dist/{BlockNoteViewer-RQBYHBQL.js → BlockNoteViewer-2KPGZSTJ.js} +3 -3
  5. package/dist/{BlockNoteViewer-RQBYHBQL.js.map → BlockNoteViewer-2KPGZSTJ.js.map} +1 -1
  6. package/dist/{BlockNoteViewer-NVSR67X5.mjs → BlockNoteViewer-XDAC6AL7.mjs} +2 -2
  7. package/dist/billing/index.js +477 -469
  8. package/dist/billing/index.js.map +1 -1
  9. package/dist/billing/index.mjs +15 -7
  10. package/dist/billing/index.mjs.map +1 -1
  11. package/dist/{chunk-5H25LM2Z.js → chunk-576XHOYH.js} +5 -5
  12. package/dist/{chunk-5H25LM2Z.js.map → chunk-576XHOYH.js.map} +1 -1
  13. package/dist/{chunk-NPGX2GJD.js → chunk-CI64OTRX.js} +98 -75
  14. package/dist/chunk-CI64OTRX.js.map +1 -0
  15. package/dist/{chunk-AEBZ67JJ.mjs → chunk-ETU3AW2X.mjs} +2 -2
  16. package/dist/{chunk-4JVLLBRS.mjs → chunk-GUNH3THR.mjs} +24 -1
  17. package/dist/chunk-GUNH3THR.mjs.map +1 -0
  18. package/dist/{chunk-SQNFYMYE.js → chunk-KH4Y4FU4.js} +1532 -1288
  19. package/dist/chunk-KH4Y4FU4.js.map +1 -0
  20. package/dist/{chunk-PNCO3HMJ.js → chunk-TCAOU6NH.js} +3 -3
  21. package/dist/{chunk-PNCO3HMJ.js.map → chunk-TCAOU6NH.js.map} +1 -1
  22. package/dist/{chunk-F25BVBEH.mjs → chunk-V4NPMUZ2.mjs} +2663 -2419
  23. package/dist/chunk-V4NPMUZ2.mjs.map +1 -0
  24. package/dist/{chunk-AMQFKCAX.mjs → chunk-WPFEPIKP.mjs} +2 -2
  25. package/dist/client/index.js +6 -4
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/index.mjs +5 -3
  28. package/dist/components/index.js +12 -4
  29. package/dist/components/index.js.map +1 -1
  30. package/dist/components/index.mjs +11 -3
  31. package/dist/components/tables/ContentListTable.d.ts +14 -0
  32. package/dist/components/tables/ContentListTable.d.ts.map +1 -1
  33. package/dist/components/tables/SortableHeader.d.ts +17 -0
  34. package/dist/components/tables/SortableHeader.d.ts.map +1 -0
  35. package/dist/components/tables/cells/cell.date.d.ts +4 -0
  36. package/dist/components/tables/cells/cell.date.d.ts.map +1 -1
  37. package/dist/components/tables/cells/cell.dateTime.d.ts +4 -0
  38. package/dist/components/tables/cells/cell.dateTime.d.ts.map +1 -1
  39. package/dist/components/tables/cells/cell.link.d.ts +4 -0
  40. package/dist/components/tables/cells/cell.link.d.ts.map +1 -1
  41. package/dist/components/tables/cells/cell.text.d.ts +4 -0
  42. package/dist/components/tables/cells/cell.text.d.ts.map +1 -1
  43. package/dist/components/tables/index.d.ts +2 -0
  44. package/dist/components/tables/index.d.ts.map +1 -1
  45. package/dist/components/tables/sorting.d.ts +24 -0
  46. package/dist/components/tables/sorting.d.ts.map +1 -0
  47. package/dist/contexts/index.js +4 -4
  48. package/dist/contexts/index.mjs +3 -3
  49. package/dist/core/index.js +2 -2
  50. package/dist/core/index.mjs +1 -1
  51. package/dist/email-automations/index.js +120 -115
  52. package/dist/email-automations/index.js.map +1 -1
  53. package/dist/email-automations/index.mjs +18 -13
  54. package/dist/email-automations/index.mjs.map +1 -1
  55. package/dist/features/analytics/index.js +114 -114
  56. package/dist/features/analytics/index.mjs +3 -3
  57. package/dist/features/analytics/server.js +4 -4
  58. package/dist/features/analytics/server.mjs +2 -2
  59. package/dist/features/billing/stripe-coupon/components/lists/CouponCodesList.d.ts.map +1 -1
  60. package/dist/features/billing/stripe-coupon/components/lists/CouponsList.d.ts.map +1 -1
  61. package/dist/features/billing/stripe-coupon/data/stripe-coupon-code.service.d.ts +2 -0
  62. package/dist/features/billing/stripe-coupon/data/stripe-coupon-code.service.d.ts.map +1 -1
  63. package/dist/features/billing/stripe-coupon/data/stripe-coupon.service.d.ts +1 -0
  64. package/dist/features/billing/stripe-coupon/data/stripe-coupon.service.d.ts.map +1 -1
  65. package/dist/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.d.ts.map +1 -1
  66. package/dist/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.d.ts.map +1 -1
  67. package/dist/features/billing/stripe-price/components/lists/PricesList.d.ts.map +1 -1
  68. package/dist/features/billing/stripe-price/data/stripe-price.service.d.ts +2 -0
  69. package/dist/features/billing/stripe-price/data/stripe-price.service.d.ts.map +1 -1
  70. package/dist/features/billing/stripe-price/hooks/useStripePriceTableStructure.d.ts.map +1 -1
  71. package/dist/features/billing/stripe-product/components/lists/ProductsList.d.ts.map +1 -1
  72. package/dist/features/billing/stripe-product/data/stripe-product.service.d.ts +2 -0
  73. package/dist/features/billing/stripe-product/data/stripe-product.service.d.ts.map +1 -1
  74. package/dist/features/billing/stripe-product/hooks/useStripeProductTableStructure.d.ts.map +1 -1
  75. package/dist/features/blog/components/admin/BlogPostsList.d.ts.map +1 -1
  76. package/dist/features/blog/data/BlogPostService.d.ts +1 -0
  77. package/dist/features/blog/data/BlogPostService.d.ts.map +1 -1
  78. package/dist/features/blog/index.js +134 -132
  79. package/dist/features/blog/index.js.map +1 -1
  80. package/dist/features/blog/index.mjs +8 -6
  81. package/dist/features/blog/index.mjs.map +1 -1
  82. package/dist/features/blog/server.js +7 -7
  83. package/dist/features/blog/server.mjs +1 -1
  84. package/dist/features/company/components/lists/CompaniesList.d.ts.map +1 -1
  85. package/dist/features/company/data/company.service.d.ts +1 -0
  86. package/dist/features/company/data/company.service.d.ts.map +1 -1
  87. package/dist/features/content/data/content.service.d.ts +3 -0
  88. package/dist/features/content/data/content.service.d.ts.map +1 -1
  89. package/dist/features/email-automation/components/lists/EmailAutomationRecipientsList.d.ts.map +1 -1
  90. package/dist/features/email-automation/components/lists/EmailAutomationsList.d.ts.map +1 -1
  91. package/dist/features/email-automation/data/EmailAutomationService.d.ts +3 -0
  92. package/dist/features/email-automation/data/EmailAutomationService.d.ts.map +1 -1
  93. package/dist/features/email-automation/hooks/useEmailAutomationTableStructure.d.ts.map +1 -1
  94. package/dist/features/email-blast/components/lists/EmailBlastsList.d.ts.map +1 -1
  95. package/dist/features/email-blast/data/EmailBlastService.d.ts +1 -0
  96. package/dist/features/email-blast/data/EmailBlastService.d.ts.map +1 -1
  97. package/dist/features/email-blast/hooks/useEmailBlastTableStructure.d.ts.map +1 -1
  98. package/dist/features/help/index.js +37 -37
  99. package/dist/features/help/index.mjs +3 -3
  100. package/dist/features/how-to/components/lists/HowToList.d.ts.map +1 -1
  101. package/dist/features/how-to/data/HowToService.d.ts +2 -0
  102. package/dist/features/how-to/data/HowToService.d.ts.map +1 -1
  103. package/dist/features/notification/components/lists/NotificationsList.d.ts.map +1 -1
  104. package/dist/features/notification/data/notification.service.d.ts +2 -0
  105. package/dist/features/notification/data/notification.service.d.ts.map +1 -1
  106. package/dist/features/referral/components/containers/ReferralsAdminContainer.d.ts.map +1 -1
  107. package/dist/features/referral/data/ReferralAdminService.d.ts +1 -0
  108. package/dist/features/referral/data/ReferralAdminService.d.ts.map +1 -1
  109. package/dist/features/referral/hooks/useReferralAdminReferralTableStructure.d.ts.map +1 -1
  110. package/dist/features/role/components/lists/RolesList.d.ts.map +1 -1
  111. package/dist/features/role/components/lists/UserRolesList.d.ts.map +1 -1
  112. package/dist/features/role/data/role.service.d.ts +3 -0
  113. package/dist/features/role/data/role.service.d.ts.map +1 -1
  114. package/dist/features/tokenusage/index.js +114 -114
  115. package/dist/features/tokenusage/index.mjs +3 -3
  116. package/dist/features/user/components/lists/AdminUsersList.d.ts.map +1 -1
  117. package/dist/features/user/components/lists/CompanyUsersList.d.ts.map +1 -1
  118. package/dist/features/user/components/lists/PlatformUsersList.d.ts.map +1 -1
  119. package/dist/features/user/components/lists/RoleUsersList.d.ts.map +1 -1
  120. package/dist/features/user/components/lists/UsersListByContentIds.d.ts.map +1 -1
  121. package/dist/features/user/data/user.service.d.ts +11 -0
  122. package/dist/features/user/data/user.service.d.ts.map +1 -1
  123. package/dist/features/waitlist/components/lists/WaitlistList.d.ts.map +1 -1
  124. package/dist/features/waitlist/data/WaitlistService.d.ts +2 -0
  125. package/dist/features/waitlist/data/WaitlistService.d.ts.map +1 -1
  126. package/dist/hooks/index.d.ts +1 -0
  127. package/dist/hooks/index.d.ts.map +1 -1
  128. package/dist/hooks/useDataListRetriever.d.ts +13 -0
  129. package/dist/hooks/useDataListRetriever.d.ts.map +1 -1
  130. package/dist/hooks/useListSortParam.d.ts +23 -0
  131. package/dist/hooks/useListSortParam.d.ts.map +1 -0
  132. package/dist/index.js +3 -3
  133. package/dist/index.mjs +2 -2
  134. package/dist/server/index.js +3 -3
  135. package/dist/server/index.mjs +2 -2
  136. package/dist/shadcnui/ui/calendar.d.ts +1 -1
  137. package/dist/shadcnui/ui/calendar.d.ts.map +1 -1
  138. package/messages/en.json +11 -0
  139. package/messages/it.json +11 -0
  140. package/package.json +1 -1
  141. package/src/components/tables/ContentListTable.tsx +108 -10
  142. package/src/components/tables/SortableHeader.tsx +43 -0
  143. package/src/components/tables/__tests__/ContentListTable.rendering.spec.tsx +1 -0
  144. package/src/components/tables/__tests__/ContentListTable.sort.spec.tsx +273 -0
  145. package/src/components/tables/__tests__/sorting.spec.ts +61 -0
  146. package/src/components/tables/cells/cell.date.tsx +10 -2
  147. package/src/components/tables/cells/cell.dateTime.tsx +10 -2
  148. package/src/components/tables/cells/cell.link.tsx +6 -1
  149. package/src/components/tables/cells/cell.text.tsx +11 -2
  150. package/src/components/tables/index.ts +2 -0
  151. package/src/components/tables/sorting.ts +41 -0
  152. package/src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx +2 -0
  153. package/src/features/billing/stripe-coupon/components/lists/CouponsList.tsx +2 -0
  154. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.service.ts +4 -0
  155. package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +7 -1
  156. package/src/features/billing/stripe-coupon/hooks/__tests__/useStripeCouponCodeTableStructure.sort.spec.tsx +30 -0
  157. package/src/features/billing/stripe-coupon/hooks/__tests__/useStripeCouponTableStructure.sort.spec.tsx +33 -0
  158. package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +5 -4
  159. package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +7 -4
  160. package/src/features/billing/stripe-price/components/lists/PricesList.tsx +2 -0
  161. package/src/features/billing/stripe-price/data/stripe-price.service.ts +4 -0
  162. package/src/features/billing/stripe-price/hooks/__tests__/useStripePriceTableStructure.sort.spec.tsx +29 -0
  163. package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +8 -5
  164. package/src/features/billing/stripe-product/components/lists/ProductsList.tsx +2 -0
  165. package/src/features/billing/stripe-product/data/stripe-product.service.ts +4 -0
  166. package/src/features/billing/stripe-product/hooks/__tests__/useStripeProductTableStructure.sort.spec.tsx +29 -0
  167. package/src/features/billing/stripe-product/hooks/useStripeProductTableStructure.tsx +3 -2
  168. package/src/features/blog/components/admin/BlogPostsList.tsx +2 -0
  169. package/src/features/blog/data/BlogPostService.ts +3 -1
  170. package/src/features/blog/hooks/__tests__/useBlogPostTableStructure.sort.spec.tsx +19 -0
  171. package/src/features/blog/hooks/useBlogPostTableStructure.tsx +2 -2
  172. package/src/features/company/components/lists/CompaniesList.tsx +2 -0
  173. package/src/features/company/data/company.service.ts +3 -1
  174. package/src/features/company/hooks/__tests__/useCompanyTableStructure.sort.spec.tsx +19 -0
  175. package/src/features/company/hooks/useCompanyTableStructure.tsx +1 -1
  176. package/src/features/content/data/content.service.ts +12 -1
  177. package/src/features/content/hooks/__tests__/useContentTableStructure.sort.spec.tsx +21 -0
  178. package/src/features/content/hooks/useContentTableStructure.tsx +1 -1
  179. package/src/features/email-automation/components/lists/EmailAutomationRecipientsList.tsx +2 -0
  180. package/src/features/email-automation/components/lists/EmailAutomationsList.tsx +2 -0
  181. package/src/features/email-automation/data/EmailAutomationService.ts +9 -1
  182. package/src/features/email-automation/hooks/__tests__/useEmailAutomationEnrollmentTableStructure.sort.spec.tsx +21 -0
  183. package/src/features/email-automation/hooks/__tests__/useEmailAutomationTableStructure.sort.spec.tsx +29 -0
  184. package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +4 -4
  185. package/src/features/email-automation/hooks/useEmailAutomationTableStructure.tsx +5 -4
  186. package/src/features/email-blast/components/lists/EmailBlastsList.tsx +2 -0
  187. package/src/features/email-blast/data/EmailBlastService.ts +5 -1
  188. package/src/features/email-blast/hooks/__tests__/useEmailBlastTableStructure.sort.spec.tsx +29 -0
  189. package/src/features/email-blast/hooks/useEmailBlastTableStructure.tsx +16 -8
  190. package/src/features/how-to/components/lists/HowToList.tsx +2 -0
  191. package/src/features/how-to/data/HowToService.ts +4 -0
  192. package/src/features/how-to/hooks/__tests__/useHowToTableStructure.sort.spec.tsx +18 -0
  193. package/src/features/how-to/hooks/useHowToTableStructure.tsx +1 -1
  194. package/src/features/notification/components/lists/NotificationsList.tsx +2 -0
  195. package/src/features/notification/data/notification.service.ts +4 -0
  196. package/src/features/notification/hooks/__tests__/useNotificationTableStructure.sort.spec.tsx +19 -0
  197. package/src/features/notification/hooks/useNotificationTableStructure.tsx +1 -1
  198. package/src/features/referral/components/containers/ReferralsAdminContainer.tsx +4 -2
  199. package/src/features/referral/data/ReferralAdminService.ts +6 -1
  200. package/src/features/referral/hooks/__tests__/useReferralAdminReferralTableStructure.sort.spec.tsx +35 -0
  201. package/src/features/referral/hooks/useReferralAdminReferralTableStructure.tsx +10 -5
  202. package/src/features/role/components/lists/RolesList.tsx +2 -0
  203. package/src/features/role/components/lists/UserRolesList.tsx +2 -0
  204. package/src/features/role/data/role.service.ts +7 -1
  205. package/src/features/role/hooks/__tests__/useRoleTableStructure.sort.spec.tsx +16 -0
  206. package/src/features/role/hooks/useRoleTableStructure.tsx +2 -2
  207. package/src/features/user/components/lists/AdminUsersList.tsx +2 -0
  208. package/src/features/user/components/lists/CompanyUsersList.tsx +4 -0
  209. package/src/features/user/components/lists/PlatformUsersList.tsx +2 -0
  210. package/src/features/user/components/lists/RoleUsersList.tsx +2 -0
  211. package/src/features/user/components/lists/UsersListByContentIds.tsx +2 -0
  212. package/src/features/user/data/user.service.ts +28 -1
  213. package/src/features/user/hooks/__tests__/useUserTableStructure.sort.spec.tsx +16 -0
  214. package/src/features/user/hooks/useUserTableStructure.tsx +2 -2
  215. package/src/features/waitlist/components/lists/WaitlistList.tsx +2 -0
  216. package/src/features/waitlist/data/WaitlistService.ts +4 -0
  217. package/src/features/waitlist/hooks/__tests__/useWaitlistTableStructure.sort.spec.tsx +17 -0
  218. package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +3 -3
  219. package/src/hooks/__tests__/useDataListRetriever.sort.spec.tsx +114 -0
  220. package/src/hooks/__tests__/useListSortParam.spec.tsx +91 -0
  221. package/src/hooks/index.ts +1 -0
  222. package/src/hooks/useDataListRetriever.ts +44 -4
  223. package/src/hooks/useListSortParam.ts +53 -0
  224. package/src/shadcnui/ui/__tests__/calendar.spec.tsx +222 -0
  225. package/src/shadcnui/ui/calendar.tsx +28 -1
  226. package/dist/chunk-4JVLLBRS.mjs.map +0 -1
  227. package/dist/chunk-F25BVBEH.mjs.map +0 -1
  228. package/dist/chunk-NPGX2GJD.js.map +0 -1
  229. package/dist/chunk-SQNFYMYE.js.map +0 -1
  230. /package/dist/{BlockNoteEditor-XIEMUQCO.mjs.map → BlockNoteEditor-DCVT7YHE.mjs.map} +0 -0
  231. /package/dist/{BlockNoteViewer-NVSR67X5.mjs.map → BlockNoteViewer-XDAC6AL7.mjs.map} +0 -0
  232. /package/dist/{chunk-AEBZ67JJ.mjs.map → chunk-ETU3AW2X.mjs.map} +0 -0
  233. /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 { ExpandedState, flexRender, getCoreRowModel, getExpandedRowModel, useReactTable } from "@tanstack/react-table";
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: data?.data ?? EMPTY_ARRAY,
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 { className?: string } | undefined;
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 key={header.id} className={meta?.className}>
297
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
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 { className?: string } | undefined;
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 { className?: string } | undefined;
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
+ }
@@ -52,6 +52,7 @@ function createMockDataRetriever(overrides: Partial<DataListRetriever<any>> = {}
52
52
  removeAdditionalParameter: vi.fn(),
53
53
  setRefreshedElement: vi.fn(),
54
54
  removeElement: vi.fn(),
55
+ setSort: vi.fn(),
55
56
  isSearch: false,
56
57
  ...overrides,
57
58
  };
@@ -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: { name: string; title: string }): ColumnDef<any> => {
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: false,
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: { name: string; title: string }): ColumnDef<any> => {
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: false,
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: false,
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: { id: string; name: string; title: string }): ColumnDef<any> => {
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: false,
17
+ enableSorting: params.sortable === true,
18
+ ...(params.sortFields && { meta: { sortFields: params.sortFields } }),
10
19
  enableHiding: false,
11
20
  };
12
21
  };