@carlonicora/nextjs-jsonapi 6.32.2 → 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 (227) hide show
  1. package/dist/{BlockNoteEditor-KRNC6EAX.mjs → BlockNoteEditor-DCVT7YHE.mjs} +4 -4
  2. package/dist/{BlockNoteEditor-VXSVY6OY.js → BlockNoteEditor-ZVK5MD34.js} +24 -24
  3. package/dist/{BlockNoteEditor-VXSVY6OY.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-GPVLFNLB.js → chunk-KH4Y4FU4.js} +1411 -1187
  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-4SXHRANW.mjs → chunk-V4NPMUZ2.mjs} +2627 -2403
  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/package.json +1 -1
  137. package/src/components/tables/ContentListTable.tsx +108 -10
  138. package/src/components/tables/SortableHeader.tsx +43 -0
  139. package/src/components/tables/__tests__/ContentListTable.rendering.spec.tsx +1 -0
  140. package/src/components/tables/__tests__/ContentListTable.sort.spec.tsx +273 -0
  141. package/src/components/tables/__tests__/sorting.spec.ts +61 -0
  142. package/src/components/tables/cells/cell.date.tsx +10 -2
  143. package/src/components/tables/cells/cell.dateTime.tsx +10 -2
  144. package/src/components/tables/cells/cell.link.tsx +6 -1
  145. package/src/components/tables/cells/cell.text.tsx +11 -2
  146. package/src/components/tables/index.ts +2 -0
  147. package/src/components/tables/sorting.ts +41 -0
  148. package/src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx +2 -0
  149. package/src/features/billing/stripe-coupon/components/lists/CouponsList.tsx +2 -0
  150. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.service.ts +4 -0
  151. package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +7 -1
  152. package/src/features/billing/stripe-coupon/hooks/__tests__/useStripeCouponCodeTableStructure.sort.spec.tsx +30 -0
  153. package/src/features/billing/stripe-coupon/hooks/__tests__/useStripeCouponTableStructure.sort.spec.tsx +33 -0
  154. package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +5 -4
  155. package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +7 -4
  156. package/src/features/billing/stripe-price/components/lists/PricesList.tsx +2 -0
  157. package/src/features/billing/stripe-price/data/stripe-price.service.ts +4 -0
  158. package/src/features/billing/stripe-price/hooks/__tests__/useStripePriceTableStructure.sort.spec.tsx +29 -0
  159. package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +8 -5
  160. package/src/features/billing/stripe-product/components/lists/ProductsList.tsx +2 -0
  161. package/src/features/billing/stripe-product/data/stripe-product.service.ts +4 -0
  162. package/src/features/billing/stripe-product/hooks/__tests__/useStripeProductTableStructure.sort.spec.tsx +29 -0
  163. package/src/features/billing/stripe-product/hooks/useStripeProductTableStructure.tsx +3 -2
  164. package/src/features/blog/components/admin/BlogPostsList.tsx +2 -0
  165. package/src/features/blog/data/BlogPostService.ts +3 -1
  166. package/src/features/blog/hooks/__tests__/useBlogPostTableStructure.sort.spec.tsx +19 -0
  167. package/src/features/blog/hooks/useBlogPostTableStructure.tsx +2 -2
  168. package/src/features/company/components/lists/CompaniesList.tsx +2 -0
  169. package/src/features/company/data/company.service.ts +3 -1
  170. package/src/features/company/hooks/__tests__/useCompanyTableStructure.sort.spec.tsx +19 -0
  171. package/src/features/company/hooks/useCompanyTableStructure.tsx +1 -1
  172. package/src/features/content/data/content.service.ts +12 -1
  173. package/src/features/content/hooks/__tests__/useContentTableStructure.sort.spec.tsx +21 -0
  174. package/src/features/content/hooks/useContentTableStructure.tsx +1 -1
  175. package/src/features/email-automation/components/lists/EmailAutomationRecipientsList.tsx +2 -0
  176. package/src/features/email-automation/components/lists/EmailAutomationsList.tsx +2 -0
  177. package/src/features/email-automation/data/EmailAutomationService.ts +9 -1
  178. package/src/features/email-automation/hooks/__tests__/useEmailAutomationEnrollmentTableStructure.sort.spec.tsx +21 -0
  179. package/src/features/email-automation/hooks/__tests__/useEmailAutomationTableStructure.sort.spec.tsx +29 -0
  180. package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +4 -4
  181. package/src/features/email-automation/hooks/useEmailAutomationTableStructure.tsx +5 -4
  182. package/src/features/email-blast/components/lists/EmailBlastsList.tsx +2 -0
  183. package/src/features/email-blast/data/EmailBlastService.ts +5 -1
  184. package/src/features/email-blast/hooks/__tests__/useEmailBlastTableStructure.sort.spec.tsx +29 -0
  185. package/src/features/email-blast/hooks/useEmailBlastTableStructure.tsx +16 -8
  186. package/src/features/how-to/components/lists/HowToList.tsx +2 -0
  187. package/src/features/how-to/data/HowToService.ts +4 -0
  188. package/src/features/how-to/hooks/__tests__/useHowToTableStructure.sort.spec.tsx +18 -0
  189. package/src/features/how-to/hooks/useHowToTableStructure.tsx +1 -1
  190. package/src/features/notification/components/lists/NotificationsList.tsx +2 -0
  191. package/src/features/notification/data/notification.service.ts +4 -0
  192. package/src/features/notification/hooks/__tests__/useNotificationTableStructure.sort.spec.tsx +19 -0
  193. package/src/features/notification/hooks/useNotificationTableStructure.tsx +1 -1
  194. package/src/features/referral/components/containers/ReferralsAdminContainer.tsx +4 -2
  195. package/src/features/referral/data/ReferralAdminService.ts +6 -1
  196. package/src/features/referral/hooks/__tests__/useReferralAdminReferralTableStructure.sort.spec.tsx +35 -0
  197. package/src/features/referral/hooks/useReferralAdminReferralTableStructure.tsx +10 -5
  198. package/src/features/role/components/lists/RolesList.tsx +2 -0
  199. package/src/features/role/components/lists/UserRolesList.tsx +2 -0
  200. package/src/features/role/data/role.service.ts +7 -1
  201. package/src/features/role/hooks/__tests__/useRoleTableStructure.sort.spec.tsx +16 -0
  202. package/src/features/role/hooks/useRoleTableStructure.tsx +2 -2
  203. package/src/features/user/components/lists/AdminUsersList.tsx +2 -0
  204. package/src/features/user/components/lists/CompanyUsersList.tsx +4 -0
  205. package/src/features/user/components/lists/PlatformUsersList.tsx +2 -0
  206. package/src/features/user/components/lists/RoleUsersList.tsx +2 -0
  207. package/src/features/user/components/lists/UsersListByContentIds.tsx +2 -0
  208. package/src/features/user/data/user.service.ts +28 -1
  209. package/src/features/user/hooks/__tests__/useUserTableStructure.sort.spec.tsx +16 -0
  210. package/src/features/user/hooks/useUserTableStructure.tsx +2 -2
  211. package/src/features/waitlist/components/lists/WaitlistList.tsx +2 -0
  212. package/src/features/waitlist/data/WaitlistService.ts +4 -0
  213. package/src/features/waitlist/hooks/__tests__/useWaitlistTableStructure.sort.spec.tsx +17 -0
  214. package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +3 -3
  215. package/src/hooks/__tests__/useDataListRetriever.sort.spec.tsx +114 -0
  216. package/src/hooks/__tests__/useListSortParam.spec.tsx +91 -0
  217. package/src/hooks/index.ts +1 -0
  218. package/src/hooks/useDataListRetriever.ts +44 -4
  219. package/src/hooks/useListSortParam.ts +53 -0
  220. package/dist/chunk-4JVLLBRS.mjs.map +0 -1
  221. package/dist/chunk-4SXHRANW.mjs.map +0 -1
  222. package/dist/chunk-GPVLFNLB.js.map +0 -1
  223. package/dist/chunk-NPGX2GJD.js.map +0 -1
  224. /package/dist/{BlockNoteEditor-KRNC6EAX.mjs.map → BlockNoteEditor-DCVT7YHE.mjs.map} +0 -0
  225. /package/dist/{BlockNoteViewer-NVSR67X5.mjs.map → BlockNoteViewer-XDAC6AL7.mjs.map} +0 -0
  226. /package/dist/{chunk-AEBZ67JJ.mjs.map → chunk-ETU3AW2X.mjs.map} +0 -0
  227. /package/dist/{chunk-AMQFKCAX.mjs.map → chunk-WPFEPIKP.mjs.map} +0 -0
@@ -32,6 +32,8 @@ export function EmailBlastsList({ fullWidth }: EmailBlastsListProps = {}) {
32
32
  retriever: (params) => EmailBlastService.findMany(params),
33
33
  retrieverParams: {},
34
34
  module: Modules.EmailBlast,
35
+ // Display only (call C8): the server's own order when no sort is chosen.
36
+ defaultSort: "-updatedAt",
35
37
  });
36
38
 
37
39
  const functions: ReactNode[] = [<EmailBlastEditor key="create-email-blast" />];
@@ -4,13 +4,17 @@ import { EmailBlastAudienceInterface } from "./EmailBlastAudienceInterface";
4
4
  import { EmailBlastInput, EmailBlastInterface } from "./EmailBlastInterface";
5
5
 
6
6
  export class EmailBlastService extends AbstractService {
7
- static async findMany(params: { next?: NextRef; previous?: PreviousRef } = {}): Promise<EmailBlastInterface[]> {
7
+ static async findMany(
8
+ params: { next?: NextRef; previous?: PreviousRef; sort?: string } = {},
9
+ ): Promise<EmailBlastInterface[]> {
8
10
  const endpoint = new EndpointCreator({ endpoint: Modules.EmailBlast });
9
11
 
10
12
  if (Modules.EmailBlast.inclusions?.lists?.fields)
11
13
  endpoint.limitToFields(Modules.EmailBlast.inclusions.lists.fields);
12
14
  if (Modules.EmailBlast.inclusions?.lists?.types) endpoint.limitToType(Modules.EmailBlast.inclusions.lists.types);
13
15
 
16
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
17
+
14
18
  return this.callApi<EmailBlastInterface[]>({
15
19
  type: Modules.EmailBlast,
16
20
  method: HttpMethod.GET,
@@ -0,0 +1,29 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { EmailBlastFields } from "../../data/email-blast.fields";
4
+ import { useEmailBlastTableStructure } from "../useEmailBlastTableStructure";
5
+
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
7
+
8
+ /** Spec row A17: every column, the counts mapped to their stored property. Only an explicit `enableSorting: true` opts a column in (call C3). */
9
+ describe("useEmailBlastTableStructure sorting", () => {
10
+ it("opts in exactly the row's sortable columns", () => {
11
+ const { result } = renderHook(() =>
12
+ useEmailBlastTableStructure({ data: [], fields: Object.values(EmailBlastFields) as any }),
13
+ );
14
+ const columns = result.current.columns as any[];
15
+ const sortable = columns.filter((column) => column.enableSorting === true).map((column) => column.id);
16
+ expect(sortable.sort()).toEqual(["failed", "purpose", "sent", "sentAt", "status", "subject", "unsubscribed"]);
17
+ });
18
+
19
+ it("maps the columns whose key differs from the stored property (call C4)", () => {
20
+ const { result } = renderHook(() =>
21
+ useEmailBlastTableStructure({ data: [], fields: Object.values(EmailBlastFields) as any }),
22
+ );
23
+ const columns = result.current.columns as any[];
24
+ const sortFields = Object.fromEntries(
25
+ columns.filter((column) => column.meta?.sortFields).map((column) => [column.id, column.meta.sortFields]),
26
+ );
27
+ expect(sortFields).toEqual({ sent: ["sentCount"], failed: ["failedCount"], unsubscribed: ["unsubscribedCount"] });
28
+ });
29
+ });
@@ -33,7 +33,13 @@ export const useEmailBlastTableStructure: UseTableStructureHook<EmailBlastInterf
33
33
 
34
34
  // Typography role 15 (numeric): tabular-nums, end-aligned. NEVER
35
35
  // font-mono — no monospace font is loaded in these apps.
36
- const numericColumn = (field: EmailBlastFields, header: string, read: (b: EmailBlastInterface) => number) => ({
36
+ // `sortField` is the stored count the column shows (its id differs from the property).
37
+ const numericColumn = (
38
+ field: EmailBlastFields,
39
+ header: string,
40
+ read: (b: EmailBlastInterface) => number,
41
+ sortField: string,
42
+ ) => ({
37
43
  id: field,
38
44
  accessorKey: field,
39
45
  header: header,
@@ -41,7 +47,8 @@ export const useEmailBlastTableStructure: UseTableStructureHook<EmailBlastInterf
41
47
  const blast: EmailBlastInterface = row.original.jsonApiData;
42
48
  return <span className="block text-xs tabular-nums text-end">{read(blast) ?? 0}</span>;
43
49
  },
44
- enableSorting: false,
50
+ meta: { sortFields: [sortField] },
51
+ enableSorting: true,
45
52
  enableHiding: false,
46
53
  });
47
54
 
@@ -54,7 +61,7 @@ export const useEmailBlastTableStructure: UseTableStructureHook<EmailBlastInterf
54
61
  const blast: EmailBlastInterface = row.original.jsonApiData;
55
62
  return <Link href={generateUrl({ page: Modules.EmailBlast, id: blast.id })}>{blast.subject}</Link>;
56
63
  },
57
- enableSorting: false,
64
+ enableSorting: true,
58
65
  enableHiding: false,
59
66
  }),
60
67
  [EmailBlastFields.purpose]: () => ({
@@ -69,7 +76,7 @@ export const useEmailBlastTableStructure: UseTableStructureHook<EmailBlastInterf
69
76
  <Badge variant="softGray">{t("emailPurpose.marketing")}</Badge>
70
77
  );
71
78
  },
72
- enableSorting: false,
79
+ enableSorting: true,
73
80
  enableHiding: false,
74
81
  }),
75
82
  [EmailBlastFields.status]: () => ({
@@ -80,7 +87,7 @@ export const useEmailBlastTableStructure: UseTableStructureHook<EmailBlastInterf
80
87
  const blast: EmailBlastInterface = row.original.jsonApiData;
81
88
  return <Badge variant={STATUS_VARIANTS[blast.status]}>{t(`emailBlasts.list.status.${blast.status}`)}</Badge>;
82
89
  },
83
- enableSorting: false,
90
+ enableSorting: true,
84
91
  enableHiding: false,
85
92
  }),
86
93
  [EmailBlastFields.sentAt]: () => ({
@@ -92,18 +99,19 @@ export const useEmailBlastTableStructure: UseTableStructureHook<EmailBlastInterf
92
99
  if (!blast.sentAt) return <span className="text-muted-foreground text-xs">—</span>;
93
100
  return <span className="text-sm">{dates.format(blast.sentAt, "date")}</span>;
94
101
  },
95
- enableSorting: false,
102
+ enableSorting: true,
96
103
  enableHiding: false,
97
104
  }),
98
105
  [EmailBlastFields.sent]: () =>
99
- numericColumn(EmailBlastFields.sent, t("emailBlasts.list.columns.sent"), (b) => b.sentCount),
106
+ numericColumn(EmailBlastFields.sent, t("emailBlasts.list.columns.sent"), (b) => b.sentCount, "sentCount"),
100
107
  [EmailBlastFields.failed]: () =>
101
- numericColumn(EmailBlastFields.failed, t("emailBlasts.list.columns.failed"), (b) => b.failedCount),
108
+ numericColumn(EmailBlastFields.failed, t("emailBlasts.list.columns.failed"), (b) => b.failedCount, "failedCount"),
102
109
  [EmailBlastFields.unsubscribed]: () =>
103
110
  numericColumn(
104
111
  EmailBlastFields.unsubscribed,
105
112
  t("emailBlasts.list.columns.unsubscribed"),
106
113
  (b) => b.unsubscribedCount,
114
+ "unsubscribedCount",
107
115
  ),
108
116
  };
109
117
 
@@ -25,6 +25,8 @@ export default function HowToList({ fullWidth, extraFunctions }: HowToListProps)
25
25
  module: Modules.HowTo,
26
26
  retriever: (params) => HowToService.findMany(params),
27
27
  retrieverParams: {},
28
+ // Display only (call C8): the server's own order when no sort is chosen.
29
+ defaultSort: "-updatedAt",
28
30
  });
29
31
 
30
32
  const functions: ReactNode[] = [
@@ -16,6 +16,8 @@ export class HowToService extends AbstractService {
16
16
  fetchAll?: boolean;
17
17
  next?: NextRef;
18
18
  prev?: PreviousRef;
19
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
20
+ sort?: string;
19
21
  } = {},
20
22
  ): Promise<HowToInterface[]> {
21
23
  const endpoint = new EndpointCreator({ endpoint: Modules.HowTo });
@@ -25,6 +27,8 @@ export class HowToService extends AbstractService {
25
27
  if (Modules.HowTo.inclusions?.lists?.fields) endpoint.limitToFields(Modules.HowTo.inclusions.lists.fields);
26
28
  if (Modules.HowTo.inclusions?.lists?.types) endpoint.limitToType(Modules.HowTo.inclusions.lists.types);
27
29
 
30
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
31
+
28
32
  return this.callApi({
29
33
  type: Modules.HowTo,
30
34
  method: HttpMethod.GET,
@@ -0,0 +1,18 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { HowToFields } from "../../data/HowToFields";
4
+ import { useHowToTableStructure } from "../useHowToTableStructure";
5
+
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
7
+
8
+ /** Spec row A19: name. Only an explicit `enableSorting: true` opts a column in (call C3). */
9
+ describe("useHowToTableStructure sorting", () => {
10
+ it("opts in exactly the row's sortable columns", () => {
11
+ const { result } = renderHook(() =>
12
+ useHowToTableStructure({ data: [], fields: Object.values(HowToFields) as any }),
13
+ );
14
+ const columns = result.current.columns as any[];
15
+ const sortable = columns.filter((column) => column.enableSorting === true).map((column) => column.id);
16
+ expect(sortable.sort()).toEqual(["name"]);
17
+ });
18
+ });
@@ -43,7 +43,7 @@ export const useHowToTableStructure: UseTableStructureHook<HowToInterface, HowTo
43
43
  const howTo: HowToInterface = row.original.jsonApiData;
44
44
  return <Link href={generateUrl({ page: Modules.HowTo, id: howTo.id })}>{howTo.name}</Link>;
45
45
  },
46
- enableSorting: false,
46
+ enableSorting: true,
47
47
  enableHiding: false,
48
48
  }),
49
49
  [HowToFields.description]: () => ({
@@ -29,6 +29,8 @@ export function NotificationsList({ archived, headerControl, fullWidth }: Notifi
29
29
  module: Modules.Notification,
30
30
  retriever: (params) => NotificationService.findMany(params),
31
31
  retrieverParams: { isArchived: archived },
32
+ // Display only (call C8): the server's own order when no sort is chosen.
33
+ defaultSort: "-createdAt",
32
34
  });
33
35
 
34
36
  /* Mark read what this list ACTUALLY SHOWS, on every page. Reading the shared
@@ -6,11 +6,15 @@ export class NotificationService extends AbstractService {
6
6
  isArchived?: boolean;
7
7
  next?: NextRef;
8
8
  suppressGlobalError?: boolean;
9
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
10
+ sort?: string;
9
11
  }): Promise<NotificationInterface[]> {
10
12
  const endpoint = new EndpointCreator({ endpoint: Modules.Notification });
11
13
 
12
14
  if (params.isArchived) endpoint.addAdditionalParam("isArchived", "true");
13
15
 
16
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
17
+
14
18
  return this.callApi<NotificationInterface[]>({
15
19
  type: Modules.Notification,
16
20
  method: HttpMethod.GET,
@@ -0,0 +1,19 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { NotificationFields } from "../../data/notification.fields";
4
+ import { useNotificationTableStructure } from "../useNotificationTableStructure";
5
+
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
7
+ vi.mock("../../contexts/NotificationContext", () => ({ useNotificationContext: () => ({}) }));
8
+
9
+ /** Spec row A6: createdAt. Only an explicit `enableSorting: true` opts a column in (call C3). */
10
+ describe("useNotificationTableStructure sorting", () => {
11
+ it("opts in exactly the row's sortable columns", () => {
12
+ const { result } = renderHook(() =>
13
+ useNotificationTableStructure({ data: [], fields: Object.values(NotificationFields) as any }),
14
+ );
15
+ const columns = result.current.columns as any[];
16
+ const sortable = columns.filter((column) => column.enableSorting === true).map((column) => column.id);
17
+ expect(sortable.sort()).toEqual(["createdAt"]);
18
+ });
19
+ });
@@ -130,7 +130,7 @@ export const useNotificationTableStructure: UseTableStructureHook<NotificationIn
130
130
  </span>
131
131
  );
132
132
  },
133
- enableSorting: false,
133
+ enableSorting: true,
134
134
  enableHiding: false,
135
135
  }),
136
136
  [NotificationFields.actions]: () => ({
@@ -103,9 +103,11 @@ export function ReferralsAdminContainer() {
103
103
 
104
104
  const referrals: DataListRetriever<ReferralAdminReferralInterface> = useDataListRetriever({
105
105
  module: Modules.ReferralAdminReferral,
106
- retriever: (params: { period: ReferralAdminPeriod }) =>
107
- ReferralAdminService.getReferrals({ period: params.period }),
106
+ retriever: (params: { period: ReferralAdminPeriod; sort?: string }) =>
107
+ ReferralAdminService.getReferrals({ period: params.period, sort: params.sort }),
108
108
  retrieverParams: { period },
109
+ // Display only (call C8): the server's own order when no sort is chosen.
110
+ defaultSort: "-referredAt",
109
111
  });
110
112
 
111
113
  useRefetchOnChange(referrers as DataListRetriever<unknown>, "period", period);
@@ -38,10 +38,15 @@ export class ReferralAdminService extends AbstractService {
38
38
  }
39
39
 
40
40
  /** Every referral in the period, newest first. */
41
- static async getReferrals(params: { period: ReferralAdminPeriod }): Promise<ReferralAdminReferralInterface[]> {
41
+ static async getReferrals(params: {
42
+ period: ReferralAdminPeriod;
43
+ sort?: string;
44
+ }): Promise<ReferralAdminReferralInterface[]> {
42
45
  const endpoint = new EndpointCreator({ endpoint: Modules.ReferralAdminReferral });
43
46
  endpoint.addAdditionalParam("period", params.period);
44
47
 
48
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
49
+
45
50
  return this.callApi<ReferralAdminReferralInterface[]>({
46
51
  type: Modules.ReferralAdminReferral,
47
52
  method: HttpMethod.GET,
@@ -0,0 +1,35 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ // Loads the core barrel first, as the app does: the hook reaches it through the components barrel,
4
+ // which on its own resolves a model module before its base class (circular import).
5
+ import "../../../../core";
6
+ import "../../../../components";
7
+ import {
8
+ ReferralAdminReferralFields,
9
+ useReferralAdminReferralTableStructure,
10
+ } from "../useReferralAdminReferralTableStructure";
11
+
12
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
13
+
14
+ /** Spec row A3 (Referrals): source, status, referredAt, tokensAwarded. Only an explicit `enableSorting: true` opts a column in (call C3). */
15
+ describe("useReferralAdminReferralTableStructure sorting", () => {
16
+ it("opts in exactly the row's sortable columns", () => {
17
+ const { result } = renderHook(() =>
18
+ useReferralAdminReferralTableStructure({ data: [], fields: Object.values(ReferralAdminReferralFields) as any }),
19
+ );
20
+ const columns = result.current.columns as any[];
21
+ const sortable = columns.filter((column) => column.enableSorting === true).map((column) => column.id);
22
+ expect(sortable.sort()).toEqual(["credits", "date", "source", "status"]);
23
+ });
24
+
25
+ it("maps the columns whose key differs from the stored property (call C4)", () => {
26
+ const { result } = renderHook(() =>
27
+ useReferralAdminReferralTableStructure({ data: [], fields: Object.values(ReferralAdminReferralFields) as any }),
28
+ );
29
+ const columns = result.current.columns as any[];
30
+ const sortFields = Object.fromEntries(
31
+ columns.filter((column) => column.meta?.sortFields).map((column) => [column.id, column.meta.sortFields]),
32
+ );
33
+ expect(sortFields).toEqual({ date: ["referredAt"], credits: ["tokensAwarded"] });
34
+ });
35
+ });
@@ -81,11 +81,16 @@ export const useReferralAdminReferralTableStructure: UseTableStructureHook<
81
81
  // A referral written before the source was recorded: unknown, not guessed.
82
82
  return <span className="text-muted-foreground text-xs">—</span>;
83
83
  },
84
- enableSorting: false,
84
+ enableSorting: true,
85
85
  enableHiding: false,
86
86
  }),
87
87
  [ReferralAdminReferralFields.date]: () =>
88
- cellDate({ name: ReferralAdminReferralFields.date, title: t("referral.admin.referrals.columns.date") }),
88
+ cellDate({
89
+ name: ReferralAdminReferralFields.date,
90
+ title: t("referral.admin.referrals.columns.date"),
91
+ sortable: true,
92
+ sortFields: ["referredAt"],
93
+ }),
89
94
  [ReferralAdminReferralFields.status]: () => ({
90
95
  id: ReferralAdminReferralFields.status,
91
96
  header: t("referral.admin.referrals.columns.status"),
@@ -95,7 +100,7 @@ export const useReferralAdminReferralTableStructure: UseTableStructureHook<
95
100
  ) : (
96
101
  <Badge variant="softYellow">{t("referral.admin.status.pending")}</Badge>
97
102
  ),
98
- enableSorting: false,
103
+ enableSorting: true,
99
104
  enableHiding: false,
100
105
  }),
101
106
  [ReferralAdminReferralFields.credits]: () => ({
@@ -109,8 +114,8 @@ export const useReferralAdminReferralTableStructure: UseTableStructureHook<
109
114
  const award = decimal(entry.tokensAwarded, 0);
110
115
  return <span className="block text-xs tabular-nums text-end">{`${award} + ${award}`}</span>;
111
116
  },
112
- meta: { className: "text-end" },
113
- enableSorting: false,
117
+ meta: { className: "text-end", sortFields: ["tokensAwarded"] },
118
+ enableSorting: true,
114
119
  enableHiding: false,
115
120
  }),
116
121
  };
@@ -15,6 +15,8 @@ export function RolesList() {
15
15
  retriever: (params) => RoleService.findAllRoles(params),
16
16
  retrieverParams: {},
17
17
  module: Modules.Role,
18
+ // Display only (call C8): the server's own order when no sort is chosen.
19
+ defaultSort: "name",
18
20
  });
19
21
 
20
22
  return (
@@ -19,6 +19,8 @@ export function UserRolesList({ user }: UserRolesListProps) {
19
19
  retriever: (params) => RoleService.findAllRolesByUser(params),
20
20
  retrieverParams: { userId: user.id },
21
21
  module: Modules.Role,
22
+ // Display only (call C8): the server's own order when no sort is chosen.
23
+ defaultSort: "name",
22
24
  });
23
25
 
24
26
  return (
@@ -10,11 +10,13 @@ export class RoleService extends AbstractService {
10
10
  });
11
11
  }
12
12
 
13
- static async findAllRoles(params: { search?: string; next?: NextRef }): Promise<RoleInterface[]> {
13
+ static async findAllRoles(params: { search?: string; next?: NextRef; sort?: string }): Promise<RoleInterface[]> {
14
14
  const endpoint = new EndpointCreator({ endpoint: Modules.Role });
15
15
 
16
16
  if (params.search) endpoint.addAdditionalParam("search", params.search);
17
17
 
18
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
19
+
18
20
  return this.callApi({
19
21
  type: Modules.Role,
20
22
  method: HttpMethod.GET,
@@ -28,11 +30,15 @@ export class RoleService extends AbstractService {
28
30
  search?: string;
29
31
  next?: NextRef;
30
32
  prev?: PreviousRef;
33
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
34
+ sort?: string;
31
35
  }): Promise<RoleInterface[]> {
32
36
  const endpoint = new EndpointCreator({ endpoint: Modules.User, id: params.userId, childEndpoint: Modules.Role });
33
37
 
34
38
  if (params.search) endpoint.addAdditionalParam("search", params.search);
35
39
 
40
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
41
+
36
42
  return this.callApi({
37
43
  type: Modules.Role,
38
44
  method: HttpMethod.GET,
@@ -0,0 +1,16 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { RoleFields } from "../../data";
4
+ import { useRoleTableStructure } from "../useRoleTableStructure";
5
+
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
7
+
8
+ /** Spec rows A25, A26: name, description. Only an explicit `enableSorting: true` opts a column in (call C3). */
9
+ describe("useRoleTableStructure sorting", () => {
10
+ it("opts in exactly the row's sortable columns", () => {
11
+ const { result } = renderHook(() => useRoleTableStructure({ data: [], fields: Object.values(RoleFields) as any }));
12
+ const columns = result.current.columns as any[];
13
+ const sortable = columns.filter((column) => column.enableSorting === true).map((column) => column.id);
14
+ expect(sortable.sort()).toEqual(["description", "name"]);
15
+ });
16
+ });
@@ -42,7 +42,7 @@ export const useRoleTableStructure: UseTableStructureHook<RoleInterface, RoleFie
42
42
  const role = row.original.jsonApiData as RoleInterface;
43
43
  return <Link href={generateUrl({ page: Modules.Role, id: role.id })}>{row.getValue("name")}</Link>;
44
44
  },
45
- enableSorting: false,
45
+ enableSorting: true,
46
46
  enableHiding: false,
47
47
  }),
48
48
  [RoleFields.description]: () => ({
@@ -50,7 +50,7 @@ export const useRoleTableStructure: UseTableStructureHook<RoleInterface, RoleFie
50
50
  accessorKey: "description",
51
51
  header: t(`role.fields.description.label`),
52
52
  cell: ({ row }: { row: Row<TableContent<RoleInterface>> }) => <>{row.getValue("description")}</>,
53
- enableSorting: false,
53
+ enableSorting: true,
54
54
  enableHiding: false,
55
55
  }),
56
56
  [RoleFields.createdAt]: () =>
@@ -21,6 +21,8 @@ function AdminUsersListInternal({ company }: AdminUsersListProps) {
21
21
  retriever: (params) => UserService.findManyForAmin(params),
22
22
  retrieverParams: { companyId: company.id },
23
23
  module: Modules.User,
24
+ // Display only (call C8): the server's own order when no sort is chosen.
25
+ defaultSort: "name",
24
26
  });
25
27
 
26
28
  return (
@@ -24,6 +24,10 @@ export function CompanyUsersList({ isDeleted, fullWidth }: CompanyUsersListProps
24
24
  retriever: (params) => UserService.findAllUsers(params),
25
25
  retrieverParams: { companyId: company?.id, isDeleted: isDeleted },
26
26
  module: Modules.User,
27
+ // The active and deleted lists can share a page (AllUsersListContainer tabs): each keeps its own sort.
28
+ sortKey: isDeleted ? `${Modules.User.name}-deleted` : undefined,
29
+ // Display only (call C8): the server's own order when no sort is chosen.
30
+ defaultSort: "name",
27
31
  }) as DataListRetriever<UserInterface>;
28
32
 
29
33
  useEffect(() => {
@@ -30,6 +30,8 @@ export function PlatformUsersList({ includeDeleted, fullWidth }: PlatformUsersLi
30
30
  retriever: (params) => UserService.findMany(params),
31
31
  retrieverParams: { includeDeleted: includeDeleted },
32
32
  module: Modules.User,
33
+ // Display only (call C8): the server's own order when no sort is chosen.
34
+ defaultSort: "name",
33
35
  }) as DataListRetriever<UserInterface>;
34
36
 
35
37
  return (
@@ -19,6 +19,8 @@ export function RoleUsersList({ role }: RoleUsersListProps) {
19
19
  retriever: (params) => UserService.findAllUsersByRole(params),
20
20
  retrieverParams: { roleId: role.id },
21
21
  module: Modules.User,
22
+ // Display only (call C8): the server's own order when no sort is chosen.
23
+ defaultSort: "name",
22
24
  }) as DataListRetriever<UserInterface>;
23
25
 
24
26
  return (
@@ -18,6 +18,8 @@ export function UsersListByContentIds({ contentIds }: UsersListByContentIdsProps
18
18
  module: Modules.User,
19
19
  retriever: (params) => UserService.findManyByContentIds(params),
20
20
  retrieverParams: { contentIds: contentIds },
21
+ // Display only (call C8): the server's own order when no sort is chosen.
22
+ defaultSort: "name",
21
23
  });
22
24
 
23
25
  return (
@@ -33,6 +33,8 @@ export class UserService extends AbstractService {
33
33
  includeDeleted?: boolean;
34
34
  next?: NextRef;
35
35
  prev?: PreviousRef;
36
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
37
+ sort?: string;
36
38
  }): Promise<UserInterface[]> {
37
39
  const endpoint = new EndpointCreator({ endpoint: Modules.User });
38
40
 
@@ -42,6 +44,8 @@ export class UserService extends AbstractService {
42
44
  if (params.fetchAll) endpoint.addAdditionalParam("fetchAll", "true");
43
45
  if (params.search) endpoint.addAdditionalParam("search", params.search);
44
46
 
47
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
48
+
45
49
  return this.callApi({
46
50
  type: Modules.User,
47
51
  method: HttpMethod.GET,
@@ -55,6 +59,8 @@ export class UserService extends AbstractService {
55
59
  search?: string;
56
60
  next?: NextRef;
57
61
  prev?: PreviousRef;
62
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
63
+ sort?: string;
58
64
  }): Promise<UserInterface[]> {
59
65
  const endpoint = new EndpointCreator({ endpoint: Modules.User });
60
66
 
@@ -63,6 +69,8 @@ export class UserService extends AbstractService {
63
69
  endpoint.addAdditionalParam("fetchAll", "true");
64
70
  if (params.search) endpoint.addAdditionalParam("search", params.search);
65
71
 
72
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
73
+
66
74
  return this.callApi({
67
75
  type: Modules.User,
68
76
  method: HttpMethod.GET,
@@ -71,7 +79,12 @@ export class UserService extends AbstractService {
71
79
  });
72
80
  }
73
81
 
74
- static async findRelevant(params: { id: string; next?: NextRef; prev?: PreviousRef }): Promise<UserInterface[]> {
82
+ static async findRelevant(params: {
83
+ id: string;
84
+ next?: NextRef;
85
+ prev?: PreviousRef;
86
+ sort?: string;
87
+ }): Promise<UserInterface[]> {
75
88
  const endpoint: EndpointCreator = new EndpointCreator({
76
89
  endpoint: Modules.Content,
77
90
  id: params.id,
@@ -81,6 +94,8 @@ export class UserService extends AbstractService {
81
94
  if (Modules.User.inclusions?.lists?.fields) endpoint.limitToFields(Modules.User.inclusions.lists.fields);
82
95
  if (Modules.User.inclusions?.lists?.types) endpoint.limitToType(Modules.User.inclusions.lists.types);
83
96
 
97
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
98
+
84
99
  return this.callApi<UserInterface[]>({
85
100
  type: Modules.User,
86
101
  method: HttpMethod.GET,
@@ -94,6 +109,8 @@ export class UserService extends AbstractService {
94
109
  search?: string;
95
110
  next?: NextRef;
96
111
  prev?: PreviousRef;
112
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
113
+ sort?: string;
97
114
  }): Promise<UserInterface[]> {
98
115
  if (!params.companyId) return [];
99
116
 
@@ -105,6 +122,8 @@ export class UserService extends AbstractService {
105
122
 
106
123
  if (params.search) endpoint.addAdditionalParam("search", params.search);
107
124
 
125
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
126
+
108
127
  return this.callApi({
109
128
  type: Modules.User,
110
129
  method: HttpMethod.GET,
@@ -121,6 +140,8 @@ export class UserService extends AbstractService {
121
140
  isDeleted?: boolean;
122
141
  next?: NextRef;
123
142
  prev?: PreviousRef;
143
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
144
+ sort?: string;
124
145
  }): Promise<UserInterface[]> {
125
146
  const endpoint = new EndpointCreator({
126
147
  endpoint: Modules.Company,
@@ -133,6 +154,8 @@ export class UserService extends AbstractService {
133
154
  if (params.limitToRoles && params.limitToRoles.length > 0)
134
155
  endpoint.addAdditionalParam("limitToRoles", params.limitToRoles.join(","));
135
156
 
157
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
158
+
136
159
  return this.callApi({
137
160
  type: Modules.User,
138
161
  method: HttpMethod.GET,
@@ -146,11 +169,15 @@ export class UserService extends AbstractService {
146
169
  search?: string;
147
170
  next?: NextRef;
148
171
  prev?: PreviousRef;
172
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
173
+ sort?: string;
149
174
  }): Promise<UserInterface[]> {
150
175
  const endpoint = new EndpointCreator({ endpoint: Modules.Role, id: params.roleId, childEndpoint: Modules.User });
151
176
 
152
177
  if (params.search) endpoint.addAdditionalParam("search", params.search);
153
178
 
179
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
180
+
154
181
  return this.callApi({
155
182
  type: Modules.User,
156
183
  method: HttpMethod.GET,
@@ -0,0 +1,16 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { UserFields } from "../../data";
4
+ import { useUserTableStructure } from "../useUserTableStructure";
5
+
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
7
+
8
+ /** Spec rows A8, A9, A11, A12, A13, A15, A16: name, email. Only an explicit `enableSorting: true` opts a column in (call C3). */
9
+ describe("useUserTableStructure sorting", () => {
10
+ it("opts in exactly the row's sortable columns", () => {
11
+ const { result } = renderHook(() => useUserTableStructure({ data: [], fields: Object.values(UserFields) as any }));
12
+ const columns = result.current.columns as any[];
13
+ const sortable = columns.filter((column) => column.enableSorting === true).map((column) => column.id);
14
+ expect(sortable.sort()).toEqual(["email", "name"]);
15
+ });
16
+ });
@@ -57,7 +57,7 @@ export const useUserTableStructure: UseTableStructureHook<UserInterface, UserFie
57
57
  </Link>
58
58
  );
59
59
  },
60
- enableSorting: false,
60
+ enableSorting: true,
61
61
  enableHiding: false,
62
62
  }),
63
63
  [UserFields.email]: () => ({
@@ -65,7 +65,7 @@ export const useUserTableStructure: UseTableStructureHook<UserInterface, UserFie
65
65
  accessorKey: "email",
66
66
  header: t(`common.fields.email.label`),
67
67
  cell: ({ row }: { row: Row<TableContent<UserInterface>> }) => <>{row.getValue("email")}</>,
68
- enableSorting: false,
68
+ enableSorting: true,
69
69
  enableHiding: false,
70
70
  }),
71
71
  // Only meaningful on a platform-wide list (PlatformUsersList), where users