@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
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
4
+ import { ReactNode } from "react";
5
+ import { Button } from "../../shadcnui";
6
+ import { SortDirection } from "./sorting";
7
+
8
+ /**
9
+ * The label of a sortable column header: a button that toggles the column's sort.
10
+ *
11
+ * `direction` is the sort the user chose (primary colour). `defaultDirection` is
12
+ * the list's display-only default order, shown as a muted arrow while no sort is
13
+ * chosen. The enclosing `<TableHead>` carries `aria-sort`.
14
+ */
15
+ export function SortableHeader(props: {
16
+ columnId: string;
17
+ label: ReactNode;
18
+ direction?: SortDirection;
19
+ defaultDirection?: SortDirection;
20
+ onToggle: () => void;
21
+ }) {
22
+ const shown = props.direction ?? props.defaultDirection;
23
+ const Icon = shown === "asc" ? ArrowUp : shown === "desc" ? ArrowDown : ArrowUpDown;
24
+
25
+ return (
26
+ <Button
27
+ type="button"
28
+ variant="ghost"
29
+ size="xs"
30
+ className={props.direction ? "text-primary" : "text-muted-foreground"}
31
+ data-testid={`list-sort-header-${props.columnId}`}
32
+ onClick={props.onToggle}
33
+ >
34
+ {props.label}
35
+ <Icon
36
+ data-testid="list-sort-icon"
37
+ data-direction={shown ?? "none"}
38
+ className={props.direction ? undefined : "text-muted-foreground"}
39
+ aria-hidden="true"
40
+ />
41
+ </Button>
42
+ );
43
+ }
@@ -52,6 +52,7 @@ function createMockDataRetriever(overrides: Partial<DataListRetriever<any>> = {}
52
52
  removeAdditionalParameter: vi.fn(),
53
53
  setRefreshedElement: vi.fn(),
54
54
  removeElement: vi.fn(),
55
+ setSort: vi.fn(),
55
56
  isSearch: false,
56
57
  ...overrides,
57
58
  };
@@ -0,0 +1,273 @@
1
+ import { fireEvent, render, screen, within } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { ContentListTable } from "../ContentListTable";
4
+
5
+ // The columns the mocked table generator returns. Each test sets them; the
6
+ // generator maps every data item to a row that also carries `jsonApiData`, as
7
+ // the sibling ContentListTable specs do.
8
+ const mockColumns = vi.hoisted(() => ({ columns: [] as any[] }));
9
+
10
+ vi.mock("../../../hooks", async () => {
11
+ const actual = await vi.importActual("../../../hooks");
12
+ return {
13
+ ...actual,
14
+ useTableGenerator: vi.fn((_type: any, params: any) => ({
15
+ data: params.data.map((item: any) => ({ ...item, jsonApiData: item })),
16
+ columns: mockColumns.columns,
17
+ })),
18
+ };
19
+ });
20
+
21
+ vi.mock("../ContentTableSearch", () => ({
22
+ ContentTableSearch: () => <div data-testid="content-table-search">Search</div>,
23
+ }));
24
+
25
+ const nameColumn = (enableSorting?: boolean) => ({
26
+ id: "name",
27
+ header: "Name",
28
+ accessorKey: "name",
29
+ cell: ({ row }: any) => row.original.name,
30
+ ...(enableSorting !== undefined && { enableSorting }),
31
+ });
32
+
33
+ const updatedAtColumn = {
34
+ id: "updatedAt",
35
+ header: "Updated",
36
+ accessorKey: "updatedAt",
37
+ cell: ({ row }: any) => row.original.updatedAt,
38
+ enableSorting: true,
39
+ };
40
+
41
+ const rows = [
42
+ { id: "1", name: "Bravo", updatedAt: "2" },
43
+ { id: "2", name: "Alpha", updatedAt: "3" },
44
+ { id: "3", name: "Charlie", updatedAt: "1" },
45
+ ];
46
+
47
+ const retriever = (overrides: Record<string, any> = {}) =>
48
+ ({
49
+ data: rows,
50
+ isLoaded: true,
51
+ ready: true,
52
+ next: undefined,
53
+ previous: undefined,
54
+ refresh: vi.fn(),
55
+ addAdditionalParameter: vi.fn(),
56
+ removeAdditionalParameter: vi.fn(),
57
+ setRefreshedElement: vi.fn(),
58
+ removeElement: vi.fn(),
59
+ search: vi.fn(),
60
+ setReady: vi.fn(),
61
+ setSort: vi.fn(),
62
+ isSearch: false,
63
+ ...overrides,
64
+ }) as any;
65
+
66
+ const mockModule = { name: "things", icon: undefined } as any;
67
+
68
+ const renderedNames = () =>
69
+ screen
70
+ .getAllByRole("row")
71
+ .slice(1)
72
+ .map((row) => row.textContent);
73
+
74
+ const headCell = (columnId: string) => screen.getByTestId(`list-sort-header-${columnId}`).closest("th")!;
75
+
76
+ describe("ContentListTable sortable headers", () => {
77
+ beforeEach(() => {
78
+ window.history.replaceState(null, "", "/things");
79
+ });
80
+
81
+ afterEach(() => {
82
+ window.history.replaceState(null, "", "/");
83
+ vi.restoreAllMocks();
84
+ mockColumns.columns = [];
85
+ });
86
+
87
+ it("renders a sort button only for a column with enableSorting: true", () => {
88
+ mockColumns.columns = [nameColumn(true)];
89
+ render(<ContentListTable data={retriever()} fields={["name"]} tableGeneratorType={mockModule} />);
90
+ expect(screen.getByTestId("list-sort-header-name")).toBeInTheDocument();
91
+ });
92
+
93
+ it("renders no sort button when the column omits enableSorting", () => {
94
+ mockColumns.columns = [nameColumn()];
95
+ render(<ContentListTable data={retriever()} fields={["name"]} tableGeneratorType={mockModule} />);
96
+ expect(screen.queryByTestId("list-sort-header-name")).toBeNull();
97
+ expect(screen.getByText("Name").closest("th")).not.toHaveAttribute("aria-sort");
98
+ });
99
+
100
+ it("renders a plain header in server mode when a hand-built retriever has no setSort", () => {
101
+ mockColumns.columns = [nameColumn(true)];
102
+ render(
103
+ <ContentListTable data={retriever({ setSort: undefined })} fields={["name"]} tableGeneratorType={mockModule} />,
104
+ );
105
+ expect(screen.queryByTestId("list-sort-header-name")).toBeNull();
106
+ expect(screen.getByText("Name").closest("th")).not.toHaveAttribute("aria-sort");
107
+ });
108
+
109
+ it("still sorts in client mode when the retriever has no setSort", () => {
110
+ mockColumns.columns = [nameColumn(true)];
111
+ render(
112
+ <ContentListTable
113
+ data={retriever({ setSort: undefined })}
114
+ fields={["name"]}
115
+ tableGeneratorType={mockModule}
116
+ sortMode="client"
117
+ sortKey="things"
118
+ />,
119
+ );
120
+ fireEvent.click(screen.getByTestId("list-sort-header-name"));
121
+ expect(renderedNames()).toEqual(["Alpha", "Bravo", "Charlie"]);
122
+ });
123
+
124
+ it("renders no sort button when enableSorting is false", () => {
125
+ mockColumns.columns = [nameColumn(false)];
126
+ render(<ContentListTable data={retriever()} fields={["name"]} tableGeneratorType={mockModule} />);
127
+ expect(screen.queryByTestId("list-sort-header-name")).toBeNull();
128
+ });
129
+
130
+ it("cycles none -> asc -> desc -> none through data.setSort, with aria-sort following", () => {
131
+ mockColumns.columns = [nameColumn(true)];
132
+ const setSort = vi.fn();
133
+
134
+ const { rerender } = render(
135
+ <ContentListTable data={retriever({ setSort })} fields={["name"]} tableGeneratorType={mockModule} />,
136
+ );
137
+ expect(headCell("name")).toHaveAttribute("aria-sort", "none");
138
+ fireEvent.click(screen.getByTestId("list-sort-header-name"));
139
+ expect(setSort).toHaveBeenLastCalledWith("name");
140
+
141
+ rerender(
142
+ <ContentListTable
143
+ data={retriever({ setSort, sort: "name" })}
144
+ fields={["name"]}
145
+ tableGeneratorType={mockModule}
146
+ />,
147
+ );
148
+ expect(headCell("name")).toHaveAttribute("aria-sort", "ascending");
149
+ fireEvent.click(screen.getByTestId("list-sort-header-name"));
150
+ expect(setSort).toHaveBeenLastCalledWith("-name");
151
+
152
+ rerender(
153
+ <ContentListTable
154
+ data={retriever({ setSort, sort: "-name" })}
155
+ fields={["name"]}
156
+ tableGeneratorType={mockModule}
157
+ />,
158
+ );
159
+ expect(headCell("name")).toHaveAttribute("aria-sort", "descending");
160
+ fireEvent.click(screen.getByTestId("list-sort-header-name"));
161
+ expect(setSort).toHaveBeenLastCalledWith(null);
162
+ expect(setSort).toHaveBeenCalledTimes(3);
163
+ });
164
+
165
+ it("does not reorder rows in server mode", () => {
166
+ mockColumns.columns = [nameColumn(true)];
167
+ render(<ContentListTable data={retriever({ sort: "name" })} fields={["name"]} tableGeneratorType={mockModule} />);
168
+ expect(renderedNames()).toEqual(["Bravo", "Alpha", "Charlie"]);
169
+ });
170
+
171
+ it("shows the defaultSort column with a muted arrow and aria-sort none", () => {
172
+ mockColumns.columns = [nameColumn(true), updatedAtColumn];
173
+ render(
174
+ <ContentListTable
175
+ data={retriever({ defaultSort: "-updatedAt" })}
176
+ fields={["name", "updatedAt"]}
177
+ tableGeneratorType={mockModule}
178
+ />,
179
+ );
180
+
181
+ expect(headCell("updatedAt")).toHaveAttribute("aria-sort", "none");
182
+ const icon = within(screen.getByTestId("list-sort-header-updatedAt")).getByTestId("list-sort-icon");
183
+ expect(icon).toHaveAttribute("data-direction", "desc");
184
+ expect(icon.getAttribute("class")).toContain("text-muted-foreground");
185
+ expect(screen.getByTestId("list-sort-header-updatedAt").className).toContain("text-muted-foreground");
186
+
187
+ expect(within(screen.getByTestId("list-sort-header-name")).getByTestId("list-sort-icon")).toHaveAttribute(
188
+ "data-direction",
189
+ "none",
190
+ );
191
+ });
192
+
193
+ it("drops the muted default arrow once a sort is chosen", () => {
194
+ mockColumns.columns = [nameColumn(true), updatedAtColumn];
195
+ render(
196
+ <ContentListTable
197
+ data={retriever({ defaultSort: "-updatedAt", sort: "name" })}
198
+ fields={["name", "updatedAt"]}
199
+ tableGeneratorType={mockModule}
200
+ />,
201
+ );
202
+
203
+ expect(within(screen.getByTestId("list-sort-header-updatedAt")).getByTestId("list-sort-icon")).toHaveAttribute(
204
+ "data-direction",
205
+ "none",
206
+ );
207
+ expect(screen.getByTestId("list-sort-header-name").className).toContain("text-primary");
208
+ });
209
+
210
+ it("sorts the loaded rows in the browser in client mode, without calling setSort", () => {
211
+ mockColumns.columns = [nameColumn(true)];
212
+ const setSort = vi.fn();
213
+ render(
214
+ <ContentListTable
215
+ data={retriever({ setSort })}
216
+ fields={["name"]}
217
+ tableGeneratorType={mockModule}
218
+ sortMode="client"
219
+ sortKey="things"
220
+ />,
221
+ );
222
+ expect(renderedNames()).toEqual(["Bravo", "Alpha", "Charlie"]);
223
+
224
+ fireEvent.click(screen.getByTestId("list-sort-header-name"));
225
+ expect(renderedNames()).toEqual(["Alpha", "Bravo", "Charlie"]);
226
+ expect(headCell("name")).toHaveAttribute("aria-sort", "ascending");
227
+ expect(new URLSearchParams(window.location.search).get("sort[things]")).toBe("name");
228
+
229
+ fireEvent.click(screen.getByTestId("list-sort-header-name"));
230
+ expect(renderedNames()).toEqual(["Charlie", "Bravo", "Alpha"]);
231
+ expect(headCell("name")).toHaveAttribute("aria-sort", "descending");
232
+
233
+ fireEvent.click(screen.getByTestId("list-sort-header-name"));
234
+ expect(renderedNames()).toEqual(["Bravo", "Alpha", "Charlie"]);
235
+ expect(headCell("name")).toHaveAttribute("aria-sort", "none");
236
+
237
+ expect(setSort).not.toHaveBeenCalled();
238
+ });
239
+
240
+ it("restores a client sort from the address on mount", () => {
241
+ window.history.replaceState(null, "", "/things?sort%5Bthings%5D=-name");
242
+ mockColumns.columns = [nameColumn(true)];
243
+ render(
244
+ <ContentListTable
245
+ data={retriever()}
246
+ fields={["name"]}
247
+ tableGeneratorType={mockModule}
248
+ sortMode="client"
249
+ sortKey="things"
250
+ />,
251
+ );
252
+ expect(renderedNames()).toEqual(["Charlie", "Bravo", "Alpha"]);
253
+ });
254
+
255
+ it("renders the order clientSort returns, called with (rows, sort)", () => {
256
+ window.history.replaceState(null, "", "/things?sort%5Bthings%5D=name");
257
+ mockColumns.columns = [nameColumn(true)];
258
+ const clientSort = vi.fn((items: any[]) => [items[2], items[0], items[1]]);
259
+ render(
260
+ <ContentListTable
261
+ data={retriever()}
262
+ fields={["name"]}
263
+ tableGeneratorType={mockModule}
264
+ sortMode="client"
265
+ sortKey="things"
266
+ clientSort={clientSort}
267
+ />,
268
+ );
269
+
270
+ expect(clientSort).toHaveBeenLastCalledWith(rows, "name");
271
+ expect(renderedNames()).toEqual(["Charlie", "Bravo", "Alpha"]);
272
+ });
273
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { nextSortValue, sortFieldsOf, sortStateFor } from "../sorting";
3
+
4
+ describe("nextSortValue", () => {
5
+ it("starts ascending when the column is not sorted", () => {
6
+ expect(nextSortValue(undefined, ["name"])).toBe("name");
7
+ });
8
+
9
+ it("goes from ascending to descending", () => {
10
+ expect(nextSortValue("name", ["name"])).toBe("-name");
11
+ });
12
+
13
+ it("clears the sort after descending", () => {
14
+ expect(nextSortValue("-name", ["name"])).toBeNull();
15
+ });
16
+
17
+ it("starts ascending when another column holds the sort", () => {
18
+ expect(nextSortValue("-createdAt", ["name"])).toBe("name");
19
+ });
20
+
21
+ it("writes every field of a composite column", () => {
22
+ expect(nextSortValue(undefined, ["lastName", "firstName"])).toBe("lastName,firstName");
23
+ expect(nextSortValue("lastName,firstName", ["lastName", "firstName"])).toBe("-lastName,-firstName");
24
+ expect(nextSortValue("-lastName,-firstName", ["lastName", "firstName"])).toBeNull();
25
+ });
26
+ });
27
+
28
+ describe("sortFieldsOf", () => {
29
+ it("uses meta.sortFields when the column declares them", () => {
30
+ expect(sortFieldsOf({ id: "name", columnDef: { meta: { sortFields: ["lastName", "firstName"] } } })).toEqual([
31
+ "lastName",
32
+ "firstName",
33
+ ]);
34
+ });
35
+
36
+ it("falls back to the column id", () => {
37
+ expect(sortFieldsOf({ id: "name", columnDef: { meta: { className: "w-10" } } })).toEqual(["name"]);
38
+ expect(sortFieldsOf({ id: "name", columnDef: {} })).toEqual(["name"]);
39
+ });
40
+
41
+ it("returns no field for a column without id or sortFields", () => {
42
+ expect(sortFieldsOf({ columnDef: {} })).toEqual([]);
43
+ });
44
+ });
45
+
46
+ describe("sortStateFor", () => {
47
+ it("reads a descending composite sort", () => {
48
+ expect(sortStateFor("-lastName,-firstName", ["lastName", "firstName"])).toBe("desc");
49
+ });
50
+
51
+ it("reads an ascending sort", () => {
52
+ expect(sortStateFor("name", ["name"])).toBe("asc");
53
+ });
54
+
55
+ it("returns undefined when the sort belongs to another column or is absent", () => {
56
+ expect(sortStateFor("-createdAt", ["name"])).toBeUndefined();
57
+ expect(sortStateFor(undefined, ["name"])).toBeUndefined();
58
+ expect(sortStateFor("lastName", ["lastName", "firstName"])).toBeUndefined();
59
+ expect(sortStateFor("name", [])).toBeUndefined();
60
+ });
61
+ });
@@ -6,7 +6,14 @@ function DateCell({ date }: { date: Date }) {
6
6
  return <span className="text-muted-foreground text-xs">{format(date, "dateMedium")}</span>;
7
7
  }
8
8
 
9
- export const cellDate = (params: { name: string; title: string }): ColumnDef<any> => {
9
+ export const cellDate = (params: {
10
+ name: string;
11
+ title: string;
12
+ /** Opt-in: the header sorts the list by this column. */
13
+ sortable?: boolean;
14
+ /** Stored fields the column sorts by, when they differ from `name`. */
15
+ sortFields?: string[];
16
+ }): ColumnDef<any> => {
10
17
  return {
11
18
  id: params.name,
12
19
  accessorKey: params.name,
@@ -16,7 +23,8 @@ export const cellDate = (params: { name: string; title: string }): ColumnDef<any
16
23
  if (!date) return null;
17
24
  return <DateCell date={date} />;
18
25
  },
19
- enableSorting: false,
26
+ enableSorting: params.sortable === true,
27
+ ...(params.sortFields && { meta: { sortFields: params.sortFields } }),
20
28
  enableHiding: false,
21
29
  };
22
30
  };
@@ -6,7 +6,14 @@ function DateTimeCell({ date }: { date: Date }) {
6
6
  return <span className="text-muted-foreground text-xs">{format(date, "dateTimeMedium")}</span>;
7
7
  }
8
8
 
9
- export const cellDateTime = (params: { name: string; title: string }): ColumnDef<any> => {
9
+ export const cellDateTime = (params: {
10
+ name: string;
11
+ title: string;
12
+ /** Opt-in: the header sorts the list by this column. */
13
+ sortable?: boolean;
14
+ /** Stored fields the column sorts by, when they differ from `name`. */
15
+ sortFields?: string[];
16
+ }): ColumnDef<any> => {
10
17
  return {
11
18
  id: params.name,
12
19
  accessorKey: params.name,
@@ -16,7 +23,8 @@ export const cellDateTime = (params: { name: string; title: string }): ColumnDef
16
23
  if (!date) return null;
17
24
  return <DateTimeCell date={date} />;
18
25
  },
19
- enableSorting: false,
26
+ enableSorting: params.sortable === true,
27
+ ...(params.sortFields && { meta: { sortFields: params.sortFields } }),
20
28
  enableHiding: false,
21
29
  };
22
30
  };
@@ -6,13 +6,18 @@ export const cellLink = <T,>(params: {
6
6
  name: string;
7
7
  title: string;
8
8
  generateUrl: (id: string) => string;
9
+ /** Opt-in: the header sorts the list by this column. */
10
+ sortable?: boolean;
11
+ /** Stored fields the column sorts by, when they differ from `name`. */
12
+ sortFields?: string[];
9
13
  }): ColumnDef<T> => {
10
14
  return {
11
15
  id: params.name,
12
16
  accessorKey: params.name,
13
17
  header: params.title,
14
18
  cell: ({ row }) => <Link href={params.generateUrl(row.getValue(params.id))}>{row.getValue(params.name)}</Link>,
15
- enableSorting: false,
19
+ enableSorting: params.sortable === true,
20
+ ...(params.sortFields && { meta: { sortFields: params.sortFields } }),
16
21
  enableHiding: false,
17
22
  };
18
23
  };
@@ -1,12 +1,21 @@
1
1
  import { ColumnDef } from "@tanstack/react-table";
2
2
 
3
- export const cellText = (params: { id: string; name: string; title: string }): ColumnDef<any> => {
3
+ export const cellText = (params: {
4
+ id: string;
5
+ name: string;
6
+ title: string;
7
+ /** Opt-in: the header sorts the list by this column. */
8
+ sortable?: boolean;
9
+ /** Stored fields the column sorts by, when they differ from `name`. */
10
+ sortFields?: string[];
11
+ }): ColumnDef<any> => {
4
12
  return {
5
13
  id: params.name,
6
14
  accessorKey: params.name,
7
15
  header: params.title,
8
16
  cell: ({ row }) => row.getValue(params.name),
9
- enableSorting: false,
17
+ enableSorting: params.sortable === true,
18
+ ...(params.sortFields && { meta: { sortFields: params.sortFields } }),
10
19
  enableHiding: false,
11
20
  };
12
21
  };
@@ -1,3 +1,5 @@
1
1
  export * from "./cells";
2
2
  export * from "./ContentListTable";
3
3
  export * from "./ContentTableSearch";
4
+ export * from "./SortableHeader";
5
+ export * from "./sorting";
@@ -0,0 +1,41 @@
1
+ /** Direction of a column sort. */
2
+ export type SortDirection = "asc" | "desc";
3
+
4
+ /**
5
+ * The fields a column sorts by: its `meta.sortFields` when declared (a column
6
+ * built from several stored fields, or whose stored property differs from its
7
+ * id), otherwise its id.
8
+ */
9
+ export function sortFieldsOf(column: { id?: string; columnDef: { meta?: unknown } }): string[] {
10
+ const meta = column.columnDef.meta as { sortFields?: unknown } | undefined;
11
+ if (Array.isArray(meta?.sortFields) && meta.sortFields.length > 0) {
12
+ return meta.sortFields.filter((field): field is string => typeof field === "string");
13
+ }
14
+ return column.id ? [column.id] : [];
15
+ }
16
+
17
+ /**
18
+ * The direction `sort` (JSON:API wire format) gives a column sorting by `fields`,
19
+ * or `undefined` when `sort` is about another column or absent.
20
+ */
21
+ export function sortStateFor(sort: string | undefined, fields: string[]): SortDirection | undefined {
22
+ if (!sort || fields.length === 0) return undefined;
23
+ if (sort === fields.join(",")) return "asc";
24
+ if (sort === fields.map((field) => `-${field}`).join(",")) return "desc";
25
+ return undefined;
26
+ }
27
+
28
+ /**
29
+ * The sort a header click sets: none → ascending → descending → none.
30
+ * Returns `null` to clear the sort.
31
+ */
32
+ export function nextSortValue(current: string | undefined, fields: string[]): string | null {
33
+ switch (sortStateFor(current, fields)) {
34
+ case undefined:
35
+ return fields.join(",");
36
+ case "asc":
37
+ return fields.map((field) => `-${field}`).join(",");
38
+ case "desc":
39
+ return null;
40
+ }
41
+ }
@@ -25,6 +25,8 @@ export function CouponCodesList({ couponId }: CouponCodesListProps) {
25
25
  retriever: (params) => StripeCouponCodeService.listByCoupon({ ...params, couponId }),
26
26
  retrieverParams: {},
27
27
  module: Modules.StripeCouponCode,
28
+ // Display only (call C8): the server's own order when no sort is chosen.
29
+ defaultSort: "createdAt",
28
30
  });
29
31
 
30
32
  // Switching "Apply automatically" on one code clears it on the others in the
@@ -31,6 +31,8 @@ export function CouponsList({ fullWidth }: CouponsListProps = {}) {
31
31
  retriever: (params) => StripeCouponService.listCoupons(params),
32
32
  retrieverParams: {},
33
33
  module: Modules.StripeCoupon,
34
+ // Display only (call C8): the server's own order when no sort is chosen.
35
+ defaultSort: "-updatedAt",
34
36
  });
35
37
 
36
38
  // Renames and code changes happen on the detail page, so the list has to
@@ -18,6 +18,8 @@ export class StripeCouponCodeService extends AbstractService {
18
18
  couponId: string;
19
19
  next?: NextRef;
20
20
  prev?: PreviousRef;
21
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
22
+ sort?: string;
21
23
  }): Promise<StripeCouponCodeInterface[]> {
22
24
  const endpoint = new EndpointCreator({
23
25
  endpoint: Modules.StripeCoupon,
@@ -25,6 +27,8 @@ export class StripeCouponCodeService extends AbstractService {
25
27
  childEndpoint: Modules.StripeCouponCode,
26
28
  });
27
29
 
30
+ if (params.sort) endpoint.addAdditionalParam("sort", params.sort);
31
+
28
32
  return this.callApi({
29
33
  type: Modules.StripeCouponCode,
30
34
  method: HttpMethod.GET,
@@ -10,11 +10,17 @@ export class StripeCouponService extends AbstractService {
10
10
  /**
11
11
  * List all coupons with their codes (admin)
12
12
  */
13
- static async listCoupons(params?: { next?: NextRef; prev?: PreviousRef }): Promise<StripeCouponInterface[]> {
13
+ static async listCoupons(params?: {
14
+ next?: NextRef;
15
+ prev?: PreviousRef;
16
+ sort?: string;
17
+ }): Promise<StripeCouponInterface[]> {
14
18
  const endpoint = new EndpointCreator({
15
19
  endpoint: Modules.StripeCoupon,
16
20
  });
17
21
 
22
+ if (params?.sort) endpoint.addAdditionalParam("sort", params?.sort);
23
+
18
24
  return this.callApi({
19
25
  type: Modules.StripeCoupon,
20
26
  method: HttpMethod.GET,
@@ -0,0 +1,30 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { StripeCouponCodeFields } from "../../data/stripe-coupon-code.fields";
4
+ import { useStripeCouponCodeTableStructure } from "../useStripeCouponCodeTableStructure";
5
+
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
7
+ vi.mock("../../../contexts/CouponContext", () => ({ useCouponContext: () => ({}) }));
8
+
9
+ /** Spec row A21: code, expiresAt, automatic, places (timesRedeemed); status is derived. Only an explicit `enableSorting: true` opts a column in (call C3). */
10
+ describe("useStripeCouponCodeTableStructure sorting", () => {
11
+ it("opts in exactly the row's sortable columns", () => {
12
+ const { result } = renderHook(() =>
13
+ useStripeCouponCodeTableStructure({ data: [], fields: Object.values(StripeCouponCodeFields) 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(["automatic", "code", "expiresAt", "places"]);
18
+ });
19
+
20
+ it("maps the columns whose key differs from the stored property (call C4)", () => {
21
+ const { result } = renderHook(() =>
22
+ useStripeCouponCodeTableStructure({ data: [], fields: Object.values(StripeCouponCodeFields) as any }),
23
+ );
24
+ const columns = result.current.columns as any[];
25
+ const sortFields = Object.fromEntries(
26
+ columns.filter((column) => column.meta?.sortFields).map((column) => [column.id, column.meta.sortFields]),
27
+ );
28
+ expect(sortFields).toEqual({ places: ["timesRedeemed"] });
29
+ });
30
+ });
@@ -0,0 +1,33 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { StripeCouponFields } from "../../data/stripe-coupon.fields";
4
+ import { useStripeCouponTableStructure } from "../useStripeCouponTableStructure";
5
+
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
7
+
8
+ /** Spec row A20: name, status (valid), places, discount; codes (a count) is not sortable. Only an explicit `enableSorting: true` opts a column in (call C3). */
9
+ describe("useStripeCouponTableStructure sorting", () => {
10
+ it("opts in exactly the row's sortable columns", () => {
11
+ const { result } = renderHook(() =>
12
+ useStripeCouponTableStructure({ data: [], fields: Object.values(StripeCouponFields) 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(["discount", "name", "places", "status"]);
17
+ });
18
+
19
+ it("maps the columns whose key differs from the stored property (call C4)", () => {
20
+ const { result } = renderHook(() =>
21
+ useStripeCouponTableStructure({ data: [], fields: Object.values(StripeCouponFields) 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({
28
+ discount: ["discountType", "percentOff", "amountOff"],
29
+ places: ["timesRedeemed", "maxRedemptions"],
30
+ status: ["valid"],
31
+ });
32
+ });
33
+ });