@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
@@ -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
@@ -32,6 +32,8 @@ export function WaitlistList({ fullWidth }: WaitlistListProps = {}) {
32
32
  retriever: (params) => WaitlistService.findMany(params),
33
33
  retrieverParams: {},
34
34
  module: Modules.Waitlist,
35
+ // Display only (call C8): the server's own order when no sort is chosen.
36
+ defaultSort: "-createdAt",
35
37
  });
36
38
 
37
39
  const loadStats = useCallback(async () => {
@@ -43,6 +43,8 @@ export class WaitlistService extends AbstractService {
43
43
  fetchAll?: boolean;
44
44
  next?: NextRef;
45
45
  previous?: PreviousRef;
46
+ /** JSON:API `sort` (`name`, `-name`), forwarded to the API. */
47
+ sort?: string;
46
48
  }): Promise<WaitlistInterface[]> {
47
49
  const endpoint = new EndpointCreator({ endpoint: Modules.Waitlist });
48
50
 
@@ -50,6 +52,8 @@ export class WaitlistService extends AbstractService {
50
52
  if (params?.search) endpoint.addAdditionalParam("search", params.search);
51
53
  if (params?.fetchAll) endpoint.addAdditionalParam("fetchAll", "true");
52
54
 
55
+ if (params?.sort) endpoint.addAdditionalParam("sort", params?.sort);
56
+
53
57
  return this.callApi<WaitlistInterface[]>({
54
58
  type: Modules.Waitlist,
55
59
  method: HttpMethod.GET,
@@ -0,0 +1,17 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { WaitlistFields } from "../../data/waitlist.fields";
4
+ import { useWaitlistTableStructure } from "../useWaitlistTableStructure";
5
+
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en" }));
7
+
8
+ /** Spec row A1: exactly email, status and createdAt are sortable (call C3: an explicit `true`). */
9
+ describe("useWaitlistTableStructure sorting", () => {
10
+ it("opts in exactly the row's sortable columns", () => {
11
+ const { result } = renderHook(() =>
12
+ useWaitlistTableStructure({ data: [], fields: Object.values(WaitlistFields) as WaitlistFields[] }),
13
+ );
14
+ const sortable = result.current.columns.filter((c) => c.enableSorting === true).map((c) => c.id);
15
+ expect(sortable.sort()).toEqual(["createdAt", "email", "status"]);
16
+ });
17
+ });
@@ -53,7 +53,7 @@ export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface,
53
53
  cell: ({ row }: { row: TableContent<WaitlistInterface> }) => (
54
54
  <span className="font-medium">{row.original.jsonApiData.email}</span>
55
55
  ),
56
- enableSorting: false,
56
+ enableSorting: true,
57
57
  enableHiding: false,
58
58
  }),
59
59
  [WaitlistFields.status]: () => ({
@@ -61,7 +61,7 @@ export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface,
61
61
  accessorKey: "status",
62
62
  header: t("waitlist.admin.columns.status"),
63
63
  cell: ({ row }: { row: TableContent<WaitlistInterface> }) => getStatusBadge(row.original.jsonApiData.status),
64
- enableSorting: false,
64
+ enableSorting: true,
65
65
  enableHiding: false,
66
66
  }),
67
67
  [WaitlistFields.createdAt]: () => ({
@@ -73,7 +73,7 @@ export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface,
73
73
  if (!entry.createdAt) return "-";
74
74
  return dates.format(entry.createdAt, "date");
75
75
  },
76
- enableSorting: false,
76
+ enableSorting: true,
77
77
  enableHiding: false,
78
78
  }),
79
79
  [WaitlistFields.source]: () => ({
@@ -0,0 +1,114 @@
1
+ import { act, renderHook, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { createMockModule } from "../../testing";
4
+ import { useDataListRetriever } from "../useDataListRetriever";
5
+
6
+ describe("useDataListRetriever sort", () => {
7
+ const mockModule = createMockModule({ name: "accounts" });
8
+
9
+ beforeEach(() => {
10
+ window.history.replaceState(null, "", "/accounts?sort%5Baccounts%5D=-name");
11
+ });
12
+
13
+ afterEach(() => {
14
+ window.history.replaceState(null, "", "/");
15
+ vi.restoreAllMocks();
16
+ });
17
+
18
+ it("reads the sort from the address and fetches once with it", async () => {
19
+ const retriever = vi.fn().mockResolvedValue([]);
20
+ const { result } = renderHook(() => useDataListRetriever({ retriever, module: mockModule }));
21
+
22
+ await waitFor(() => expect(result.current.isLoaded).toBe(true));
23
+
24
+ expect(retriever).toHaveBeenCalledTimes(1);
25
+ expect(retriever.mock.calls[0][0].sort).toBe("-name");
26
+ expect(result.current.sort).toBe("-name");
27
+ });
28
+
29
+ it("refetches once with the new sort and the current search term", async () => {
30
+ const retriever = vi.fn().mockResolvedValue([]);
31
+ const { result } = renderHook(() => useDataListRetriever({ retriever, module: mockModule }));
32
+ await waitFor(() => expect(result.current.isLoaded).toBe(true));
33
+
34
+ await act(async () => {
35
+ await result.current.search("rossi");
36
+ });
37
+ await waitFor(() => expect(result.current.isLoaded).toBe(true));
38
+ retriever.mockClear();
39
+
40
+ await act(async () => {
41
+ result.current.setSort("name");
42
+ });
43
+ await waitFor(() => expect(result.current.isLoaded).toBe(true));
44
+
45
+ expect(retriever).toHaveBeenCalledTimes(1);
46
+ expect(retriever.mock.calls[0][0].sort).toBe("name");
47
+ expect(retriever.mock.calls[0][0].search).toBe("rossi");
48
+ expect(result.current.sort).toBe("name");
49
+ expect(new URLSearchParams(window.location.search).get("sort[accounts]")).toBe("name");
50
+ });
51
+
52
+ it("returns to page 1 when the sort changes", async () => {
53
+ const retriever = vi.fn().mockImplementation(async (params: { next?: { next?: string } }) => {
54
+ if (params.next) params.next.next = "accounts?page[offset]=25";
55
+ return [];
56
+ });
57
+ const { result } = renderHook(() => useDataListRetriever({ retriever, module: mockModule }));
58
+ await waitFor(() => expect(result.current.isLoaded).toBe(true));
59
+ await waitFor(() => expect(result.current.next).toBeDefined());
60
+
61
+ retriever.mockClear();
62
+ retriever.mockResolvedValue([]);
63
+ await act(async () => {
64
+ result.current.setSort("name");
65
+ });
66
+ await waitFor(() => expect(result.current.isLoaded).toBe(true));
67
+
68
+ // The refetch goes through the list's own retriever (page 1), never the stored next-page link.
69
+ expect(retriever).toHaveBeenCalledTimes(1);
70
+ expect(retriever.mock.calls[0][0].sort).toBe("name");
71
+ expect(result.current.next).toBeUndefined();
72
+ expect(result.current.previous).toBeUndefined();
73
+ });
74
+
75
+ it("sends no sort after it is cleared", async () => {
76
+ const retriever = vi.fn().mockResolvedValue([]);
77
+ const { result } = renderHook(() => useDataListRetriever({ retriever, module: mockModule }));
78
+ await waitFor(() => expect(result.current.isLoaded).toBe(true));
79
+ retriever.mockClear();
80
+
81
+ await act(async () => {
82
+ result.current.setSort(null);
83
+ });
84
+ await waitFor(() => expect(result.current.isLoaded).toBe(true));
85
+
86
+ expect(retriever).toHaveBeenCalledTimes(1);
87
+ expect(retriever.mock.calls[0][0].sort).toBeUndefined();
88
+ expect(result.current.sort).toBeUndefined();
89
+ });
90
+
91
+ it("uses sortKey over the module name", async () => {
92
+ window.history.replaceState(null, "", "/accounts?sort%5Bclients%5D=name&sort%5Baccounts%5D=-name");
93
+ const retriever = vi.fn().mockResolvedValue([]);
94
+ const { result } = renderHook(() => useDataListRetriever({ retriever, module: mockModule, sortKey: "clients" }));
95
+ await waitFor(() => expect(result.current.isLoaded).toBe(true));
96
+
97
+ expect(retriever).toHaveBeenCalledTimes(1);
98
+ expect(retriever.mock.calls[0][0].sort).toBe("name");
99
+ });
100
+
101
+ it("exposes defaultSort and never sends it", async () => {
102
+ window.history.replaceState(null, "", "/accounts");
103
+ const retriever = vi.fn().mockResolvedValue([]);
104
+ const { result } = renderHook(() =>
105
+ useDataListRetriever({ retriever, module: mockModule, defaultSort: "-updatedAt" }),
106
+ );
107
+ await waitFor(() => expect(result.current.isLoaded).toBe(true));
108
+
109
+ expect(result.current.defaultSort).toBe("-updatedAt");
110
+ expect(result.current.sort).toBeUndefined();
111
+ expect(retriever).toHaveBeenCalledTimes(1);
112
+ expect(retriever.mock.calls[0][0].sort).toBeUndefined();
113
+ });
114
+ });
@@ -0,0 +1,91 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { useListSortParam } from "../useListSortParam";
4
+
5
+ describe("useListSortParam", () => {
6
+ beforeEach(() => {
7
+ window.history.replaceState(null, "", "/accounts?search=x");
8
+ });
9
+
10
+ afterEach(() => {
11
+ window.history.replaceState(null, "", "/");
12
+ vi.restoreAllMocks();
13
+ });
14
+
15
+ it("is not ready on the first render and ready after mount", () => {
16
+ const renders: boolean[] = [];
17
+ const { result } = renderHook(() => {
18
+ const state = useListSortParam({ sortKey: "accounts" });
19
+ renders.push(state.ready);
20
+ return state;
21
+ });
22
+
23
+ expect(renders[0]).toBe(false);
24
+ expect(result.current.ready).toBe(true);
25
+ });
26
+
27
+ it("reads its key from the address", () => {
28
+ window.history.replaceState(null, "", "/accounts?sort%5Baccounts%5D=-name");
29
+ const { result } = renderHook(() => useListSortParam({ sortKey: "accounts" }));
30
+
31
+ expect(result.current.sort).toBe("-name");
32
+ });
33
+
34
+ it("is undefined when the address has no value for its key", () => {
35
+ const { result } = renderHook(() => useListSortParam({ sortKey: "accounts" }));
36
+
37
+ expect(result.current.ready).toBe(true);
38
+ expect(result.current.sort).toBeUndefined();
39
+ });
40
+
41
+ it("writes its key with replaceState, keeping the path and the other params", () => {
42
+ const replaceState = vi.spyOn(window.history, "replaceState");
43
+ const { result } = renderHook(() => useListSortParam({ sortKey: "accounts" }));
44
+
45
+ act(() => result.current.setSort("name"));
46
+
47
+ expect(replaceState).toHaveBeenCalledTimes(1);
48
+ expect(replaceState).toHaveBeenCalledWith(null, "", "/accounts?search=x&sort%5Baccounts%5D=name");
49
+ expect(result.current.sort).toBe("name");
50
+ });
51
+
52
+ it("keeps two keys on one address independent", () => {
53
+ window.history.replaceState(null, "", "/accounts?sort%5Bcontacts%5D=-lastName");
54
+ const accounts = renderHook(() => useListSortParam({ sortKey: "accounts" }));
55
+ const contacts = renderHook(() => useListSortParam({ sortKey: "contacts" }));
56
+
57
+ expect(accounts.result.current.sort).toBeUndefined();
58
+ expect(contacts.result.current.sort).toBe("-lastName");
59
+
60
+ act(() => accounts.result.current.setSort("name"));
61
+
62
+ const params = new URLSearchParams(window.location.search);
63
+ expect(params.get("sort[accounts]")).toBe("name");
64
+ expect(params.get("sort[contacts]")).toBe("-lastName");
65
+ expect(contacts.result.current.sort).toBe("-lastName");
66
+ });
67
+
68
+ it("removes only its own key on null", () => {
69
+ window.history.replaceState(null, "", "/accounts?search=x&sort%5Baccounts%5D=name&sort%5Bcontacts%5D=-lastName");
70
+ const { result } = renderHook(() => useListSortParam({ sortKey: "accounts" }));
71
+
72
+ act(() => result.current.setSort(null));
73
+
74
+ const params = new URLSearchParams(window.location.search);
75
+ expect(params.has("sort[accounts]")).toBe(false);
76
+ expect(params.get("sort[contacts]")).toBe("-lastName");
77
+ expect(params.get("search")).toBe("x");
78
+ expect(window.location.pathname).toBe("/accounts");
79
+ expect(result.current.sort).toBeUndefined();
80
+ });
81
+
82
+ it("keeps the hash", () => {
83
+ window.history.replaceState(null, "", "/accounts#top");
84
+ const replaceState = vi.spyOn(window.history, "replaceState");
85
+ const { result } = renderHook(() => useListSortParam({ sortKey: "accounts" }));
86
+
87
+ act(() => result.current.setSort("-name"));
88
+
89
+ expect(replaceState).toHaveBeenCalledWith(null, "", "/accounts?sort%5Baccounts%5D=-name#top");
90
+ });
91
+ });
@@ -9,6 +9,7 @@ export type {
9
9
  export { useUrlRewriter } from "./url.rewriter";
10
10
  export { useDataListRetriever, type DataListRetriever } from "./useDataListRetriever";
11
11
  export { useDebounce } from "./useDebounce";
12
+ export { useListSortParam } from "./useListSortParam";
12
13
  export { usePageUrlGenerator } from "./usePageUrlGenerator";
13
14
  export { registerTableGenerator, useTableGenerator } from "./useTableGenerator";
14
15
 
@@ -3,6 +3,7 @@
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { ClientAbstractService } from "../core/abstracts/ClientAbstractService";
5
5
  import { getLastApiMeta, getLastApiTotal } from "../core/abstracts/AbstractService";
6
+ import { useListSortParam } from "./useListSortParam";
6
7
 
7
8
  export type PageInfo = {
8
9
  startItem: number;
@@ -27,6 +28,15 @@ export type DataListRetriever<T> = {
27
28
  removeElement: (element: T) => void;
28
29
  isSearch: boolean;
29
30
  pageInfo?: PageInfo;
31
+ /** The chosen sort in JSON:API wire format (`name`, `-name`), kept in the page address. */
32
+ sort?: string;
33
+ /** The list's display-only default order (e.g. `-updatedAt`). Never sent to the API. */
34
+ defaultSort?: string;
35
+ /**
36
+ * Sets (or, with `null`, clears) the sort: writes the address and refetches from page 1, keeping the search term.
37
+ * Optional so hand-built retrievers keep compiling; without it a server-mode table renders plain headers.
38
+ */
39
+ setSort?: (value: string | null) => void;
30
40
  };
31
41
 
32
42
  export function useDataListRetriever<T>(params: {
@@ -37,6 +47,10 @@ export function useDataListRetriever<T>(params: {
37
47
  addAdditionalParameter?: (key: string, value: any | null) => void;
38
48
  requiresSearch?: boolean;
39
49
  module: any;
50
+ /** Address key of this list's sort (`sort[<sortKey>]`). Defaults to `module.name`. */
51
+ sortKey?: string;
52
+ /** Display-only default order, exposed for the table header and never passed to the retriever. */
53
+ defaultSort?: string;
40
54
  }): DataListRetriever<T> {
41
55
  const [data, setData] = useState<T[] | undefined>(undefined);
42
56
  const [total, setTotal] = useState<number | undefined>(undefined);
@@ -52,6 +66,15 @@ export function useDataListRetriever<T>(params: {
52
66
  const isFetchingRef = useRef(false);
53
67
  const isPaginatingRef = useRef(false);
54
68
 
69
+ // The sort lives in the page address. The first fetch waits until the address has been
70
+ // read, so a restored sort is fetched once instead of unsorted first and sorted after.
71
+ const sortParam = useListSortParam({ sortKey: params.sortKey ?? params.module?.name ?? "" });
72
+ const sortReady = sortParam.ready;
73
+ const sortRef = useRef<string | undefined>(undefined);
74
+ useEffect(() => {
75
+ sortRef.current = sortParam.sort;
76
+ }, [sortParam.sort]);
77
+
55
78
  const resolvedType = params.module;
56
79
  const resolvedService = ClientAbstractService; // We'll just use ClientAbstractService directly for pagination
57
80
 
@@ -129,7 +152,7 @@ export function useDataListRetriever<T>(params: {
129
152
  /** Re-fetch even though a single-page list is already loaded. */
130
153
  force?: boolean;
131
154
  }) => {
132
- if (ready === false) return;
155
+ if (ready === false || !sortReady) return;
133
156
 
134
157
  // Prevent concurrent pagination calls
135
158
  if ((fetchParams?.callNext || fetchParams?.callPrevious) && isPaginatingRef.current) {
@@ -213,6 +236,7 @@ export function useDataListRetriever<T>(params: {
213
236
  // An explicit `search` passed through addAdditionalParameter wins: overwriting it
214
237
  // with the internal term (only ever set by `search()`) silently dropped it.
215
238
  if (additionalParamsRef.current.search === undefined) retrieverParams.search = currentSearchTerm;
239
+ if (sortRef.current !== undefined) retrieverParams.sort = sortRef.current;
216
240
  retrieverParams.next = nextRef;
217
241
  retrieverParams.previous = previousRef;
218
242
  retrieverParams.self = selfRef;
@@ -254,7 +278,7 @@ export function useDataListRetriever<T>(params: {
254
278
  }
255
279
  }
256
280
  },
257
- [stableParams, ready, params.search],
281
+ [stableParams, ready, sortReady, params.search],
258
282
  );
259
283
 
260
284
  const setRefreshedElement = useCallback(
@@ -297,10 +321,10 @@ export function useDataListRetriever<T>(params: {
297
321
  // and ready effect fired simultaneously on initial render
298
322
  useEffect(() => {
299
323
  // Only fetch if ready and haven't loaded yet
300
- if (ready && !isLoaded) {
324
+ if (ready && sortReady && !isLoaded) {
301
325
  fetchData({ isRefresh: true });
302
326
  }
303
- }, [ready, fetchData]);
327
+ }, [ready, sortReady, fetchData]);
304
328
 
305
329
  // Cache Components keeps a visited route alive in a hidden <Activity>: leaving it cleans up
306
330
  // its effects, coming back re-runs them with the old state intact. A loaded list would then
@@ -377,6 +401,19 @@ export function useDataListRetriever<T>(params: {
377
401
  [fetchData],
378
402
  );
379
403
 
404
+ const writeSortParam = sortParam.setSort;
405
+ const setSort = useCallback(
406
+ (value: string | null) => {
407
+ writeSortParam(value);
408
+ sortRef.current = value ? value : undefined;
409
+ setNextPage(undefined);
410
+ setPreviousPage(undefined);
411
+ // `force`: a new order must be fetched even when the loaded list fits on one page.
412
+ fetchData({ isRefine: true, isRefresh: true, force: true });
413
+ },
414
+ [fetchData, writeSortParam],
415
+ );
416
+
380
417
  const isSearch = !!searchTermRef.current;
381
418
 
382
419
  const refresh = useCallback(async () => {
@@ -412,5 +449,8 @@ export function useDataListRetriever<T>(params: {
412
449
  isSearch: isSearch,
413
450
  removeElement: removeElement,
414
451
  pageInfo: pageInfo,
452
+ sort: sortParam.sort,
453
+ defaultSort: params.defaultSort,
454
+ setSort: setSort,
415
455
  };
416
456
  }
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+
5
+ /**
6
+ * The sort of one list, kept in the page address as `sort[<sortKey>]=<value>`
7
+ * (JSON:API wire format: `name`, `-name`, `lastName,firstName`).
8
+ *
9
+ * The address is read from `window.location` once on mount, so this hook is not a
10
+ * Next search-params hook consumer and needs no `<Suspense>` boundary. `ready` is false
11
+ * until that read has happened; a loader holds its first fetch until then, so a
12
+ * restored sort costs one request, not two.
13
+ *
14
+ * `setSort` replaces only the query string of the current path with
15
+ * `history.replaceState`, keeping every other search param and the hash: no
16
+ * navigation, no scroll, no locale handling. `null` removes this list's key only.
17
+ *
18
+ * @param params.sortKey the list's address key; two lists on one page need two keys.
19
+ */
20
+ export function useListSortParam(params: { sortKey: string }): {
21
+ sort: string | undefined;
22
+ ready: boolean;
23
+ setSort: (value: string | null) => void;
24
+ } {
25
+ const { sortKey } = params;
26
+ const [sort, setSortState] = useState<string | undefined>(undefined);
27
+ const [ready, setReady] = useState(false);
28
+
29
+ useEffect(() => {
30
+ const value = new URLSearchParams(window.location.search).get(`sort[${sortKey}]`);
31
+ setSortState(value ? value : undefined);
32
+ setReady(true);
33
+ }, [sortKey]);
34
+
35
+ const setSort = useCallback(
36
+ (value: string | null) => {
37
+ const search = new URLSearchParams(window.location.search);
38
+ if (value) search.set(`sort[${sortKey}]`, value);
39
+ else search.delete(`sort[${sortKey}]`);
40
+
41
+ const next = search.toString();
42
+ window.history.replaceState(
43
+ null,
44
+ "",
45
+ `${window.location.pathname}${next ? `?${next}` : ""}${window.location.hash}`,
46
+ );
47
+ setSortState(value ? value : undefined);
48
+ },
49
+ [sortKey],
50
+ );
51
+
52
+ return { sort, ready, setSort };
53
+ }