@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
@@ -31,6 +31,8 @@ export function EmailAutomationsList({ fullWidth }: EmailAutomationsListProps =
31
31
  retriever: (params) => EmailAutomationService.findMany(params),
32
32
  retrieverParams: {},
33
33
  module: Modules.EmailAutomation,
34
+ // Display only (call C8): the server's own order when no sort is chosen.
35
+ defaultSort: "-updatedAt",
34
36
  });
35
37
 
36
38
  // The trigger column shows the label the app declares, not the stored key.
@@ -8,7 +8,9 @@ import { EmailAutomationInput, EmailAutomationInterface } from "./EmailAutomatio
8
8
  import { EmailAutomationTriggerInterface } from "./EmailAutomationTriggerInterface";
9
9
 
10
10
  export class EmailAutomationService extends AbstractService {
11
- static async findMany(params: { next?: NextRef; previous?: PreviousRef } = {}): Promise<EmailAutomationInterface[]> {
11
+ static async findMany(
12
+ params: { next?: NextRef; previous?: PreviousRef; sort?: string } = {},
13
+ ): Promise<EmailAutomationInterface[]> {
12
14
  const endpoint = new EndpointCreator({ endpoint: Modules.EmailAutomation });
13
15
 
14
16
  if (Modules.EmailAutomation.inclusions?.lists?.fields)
@@ -16,6 +18,8 @@ export class EmailAutomationService extends AbstractService {
16
18
  if (Modules.EmailAutomation.inclusions?.lists?.types)
17
19
  endpoint.limitToType(Modules.EmailAutomation.inclusions.lists.types);
18
20
 
21
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
22
+
19
23
  return this.callApi<EmailAutomationInterface[]>({
20
24
  type: Modules.EmailAutomation,
21
25
  method: HttpMethod.GET,
@@ -90,6 +94,8 @@ export class EmailAutomationService extends AbstractService {
90
94
  status?: string;
91
95
  next?: NextRef;
92
96
  previous?: PreviousRef;
97
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
98
+ sort?: string;
93
99
  }): Promise<EmailAutomationEnrollmentInterface[]> {
94
100
  const endpoint = new EndpointCreator({
95
101
  endpoint: Modules.EmailAutomation,
@@ -99,6 +105,8 @@ export class EmailAutomationService extends AbstractService {
99
105
 
100
106
  if (params.status) endpoint.addAdditionalParam("status", params.status);
101
107
 
108
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
109
+
102
110
  return this.callApi<EmailAutomationEnrollmentInterface[]>({
103
111
  type: Modules.EmailAutomationEnrollment,
104
112
  method: HttpMethod.GET,
@@ -0,0 +1,21 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { EmailAutomationEnrollmentFields } from "../../data/email-automation-enrollment.fields";
4
+ import { useEmailAutomationEnrollmentTableStructure } from "../useEmailAutomationEnrollmentTableStructure";
5
+
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
7
+
8
+ /** Spec row A4: email, name, status, nextSendAt; the computed step and sent count are not sortable. Only an explicit `enableSorting: true` opts a column in (call C3). */
9
+ describe("useEmailAutomationEnrollmentTableStructure sorting", () => {
10
+ it("opts in exactly the row's sortable columns", () => {
11
+ const { result } = renderHook(() =>
12
+ useEmailAutomationEnrollmentTableStructure({
13
+ data: [],
14
+ fields: Object.values(EmailAutomationEnrollmentFields) as any,
15
+ }),
16
+ );
17
+ const columns = result.current.columns as any[];
18
+ const sortable = columns.filter((column) => column.enableSorting === true).map((column) => column.id);
19
+ expect(sortable.sort()).toEqual(["email", "name", "nextSendAt", "status"]);
20
+ });
21
+ });
@@ -0,0 +1,29 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { EmailAutomationFields } from "../../data/email-automation.fields";
4
+ import { useEmailAutomationTableStructure } from "../useEmailAutomationTableStructure";
5
+
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
7
+
8
+ /** Spec row A5: name, purpose, trigger, status (isActive); the counts stay unsortable. Only an explicit `enableSorting: true` opts a column in (call C3). */
9
+ describe("useEmailAutomationTableStructure sorting", () => {
10
+ it("opts in exactly the row's sortable columns", () => {
11
+ const { result } = renderHook(() =>
12
+ useEmailAutomationTableStructure({ data: [], fields: Object.values(EmailAutomationFields) 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", "purpose", "status", "trigger"]);
17
+ });
18
+
19
+ it("maps the columns whose key differs from the stored property (call C4)", () => {
20
+ const { result } = renderHook(() =>
21
+ useEmailAutomationTableStructure({ data: [], fields: Object.values(EmailAutomationFields) 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({ status: ["isActive"] });
28
+ });
29
+ });
@@ -47,7 +47,7 @@ export const useEmailAutomationEnrollmentTableStructure: UseTableStructureHook<
47
47
  const enrollment: EmailAutomationEnrollmentInterface = row.original.jsonApiData;
48
48
  return <span className="text-sm">{enrollment.email}</span>;
49
49
  },
50
- enableSorting: false,
50
+ enableSorting: true,
51
51
  enableHiding: false,
52
52
  }),
53
53
  [EmailAutomationEnrollmentFields.name]: () => ({
@@ -58,7 +58,7 @@ export const useEmailAutomationEnrollmentTableStructure: UseTableStructureHook<
58
58
  const enrollment: EmailAutomationEnrollmentInterface = row.original.jsonApiData;
59
59
  return enrollment.name ? <span className="text-sm">{enrollment.name}</span> : empty;
60
60
  },
61
- enableSorting: false,
61
+ enableSorting: true,
62
62
  enableHiding: false,
63
63
  }),
64
64
  [EmailAutomationEnrollmentFields.status]: () => ({
@@ -76,7 +76,7 @@ export const useEmailAutomationEnrollmentTableStructure: UseTableStructureHook<
76
76
  </Badge>
77
77
  );
78
78
  },
79
- enableSorting: false,
79
+ enableSorting: true,
80
80
  enableHiding: false,
81
81
  }),
82
82
  [EmailAutomationEnrollmentFields.currentStep]: () => ({
@@ -103,7 +103,7 @@ export const useEmailAutomationEnrollmentTableStructure: UseTableStructureHook<
103
103
  </span>
104
104
  );
105
105
  },
106
- enableSorting: false,
106
+ enableSorting: true,
107
107
  enableHiding: false,
108
108
  }),
109
109
  [EmailAutomationEnrollmentFields.sentCount]: () => ({
@@ -59,7 +59,7 @@ export const useEmailAutomationTableStructure: UseTableStructureHook<
59
59
  const automation: EmailAutomationInterface = row.original.jsonApiData;
60
60
  return <Link href={generateUrl({ page: Modules.EmailAutomation, id: automation.id })}>{automation.name}</Link>;
61
61
  },
62
- enableSorting: false,
62
+ enableSorting: true,
63
63
  enableHiding: false,
64
64
  }),
65
65
  [EmailAutomationFields.purpose]: () => ({
@@ -78,7 +78,7 @@ export const useEmailAutomationTableStructure: UseTableStructureHook<
78
78
  </Badge>
79
79
  );
80
80
  },
81
- enableSorting: false,
81
+ enableSorting: true,
82
82
  enableHiding: false,
83
83
  }),
84
84
  [EmailAutomationFields.trigger]: () => ({
@@ -90,7 +90,7 @@ export const useEmailAutomationTableStructure: UseTableStructureHook<
90
90
  if (!automation.trigger) return <span className="text-muted-foreground text-xs">—</span>;
91
91
  return <span className="text-sm">{triggerLabels[automation.trigger] ?? automation.trigger}</span>;
92
92
  },
93
- enableSorting: false,
93
+ enableSorting: true,
94
94
  enableHiding: false,
95
95
  }),
96
96
  [EmailAutomationFields.status]: () => ({
@@ -105,7 +105,8 @@ export const useEmailAutomationTableStructure: UseTableStructureHook<
105
105
  <Badge variant="softGray">{t("emailAutomations.list.status.off")}</Badge>
106
106
  );
107
107
  },
108
- enableSorting: false,
108
+ meta: { sortFields: ["isActive"] },
109
+ enableSorting: true,
109
110
  enableHiding: false,
110
111
  }),
111
112
  [EmailAutomationFields.steps]: () =>
@@ -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(() => {