@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.
- package/dist/{BlockNoteEditor-XIEMUQCO.mjs → BlockNoteEditor-DCVT7YHE.mjs} +4 -4
- package/dist/{BlockNoteEditor-RD3JNEV5.js → BlockNoteEditor-ZVK5MD34.js} +24 -24
- package/dist/{BlockNoteEditor-RD3JNEV5.js.map → BlockNoteEditor-ZVK5MD34.js.map} +1 -1
- package/dist/{BlockNoteViewer-RQBYHBQL.js → BlockNoteViewer-2KPGZSTJ.js} +3 -3
- package/dist/{BlockNoteViewer-RQBYHBQL.js.map → BlockNoteViewer-2KPGZSTJ.js.map} +1 -1
- package/dist/{BlockNoteViewer-NVSR67X5.mjs → BlockNoteViewer-XDAC6AL7.mjs} +2 -2
- package/dist/billing/index.js +477 -469
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +15 -7
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-5H25LM2Z.js → chunk-576XHOYH.js} +5 -5
- package/dist/{chunk-5H25LM2Z.js.map → chunk-576XHOYH.js.map} +1 -1
- package/dist/{chunk-NPGX2GJD.js → chunk-CI64OTRX.js} +98 -75
- package/dist/chunk-CI64OTRX.js.map +1 -0
- package/dist/{chunk-AEBZ67JJ.mjs → chunk-ETU3AW2X.mjs} +2 -2
- package/dist/{chunk-4JVLLBRS.mjs → chunk-GUNH3THR.mjs} +24 -1
- package/dist/chunk-GUNH3THR.mjs.map +1 -0
- package/dist/{chunk-SQNFYMYE.js → chunk-KH4Y4FU4.js} +1532 -1288
- package/dist/chunk-KH4Y4FU4.js.map +1 -0
- package/dist/{chunk-PNCO3HMJ.js → chunk-TCAOU6NH.js} +3 -3
- package/dist/{chunk-PNCO3HMJ.js.map → chunk-TCAOU6NH.js.map} +1 -1
- package/dist/{chunk-F25BVBEH.mjs → chunk-V4NPMUZ2.mjs} +2663 -2419
- package/dist/chunk-V4NPMUZ2.mjs.map +1 -0
- package/dist/{chunk-AMQFKCAX.mjs → chunk-WPFEPIKP.mjs} +2 -2
- package/dist/client/index.js +6 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +5 -3
- package/dist/components/index.js +12 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +11 -3
- package/dist/components/tables/ContentListTable.d.ts +14 -0
- package/dist/components/tables/ContentListTable.d.ts.map +1 -1
- package/dist/components/tables/SortableHeader.d.ts +17 -0
- package/dist/components/tables/SortableHeader.d.ts.map +1 -0
- package/dist/components/tables/cells/cell.date.d.ts +4 -0
- package/dist/components/tables/cells/cell.date.d.ts.map +1 -1
- package/dist/components/tables/cells/cell.dateTime.d.ts +4 -0
- package/dist/components/tables/cells/cell.dateTime.d.ts.map +1 -1
- package/dist/components/tables/cells/cell.link.d.ts +4 -0
- package/dist/components/tables/cells/cell.link.d.ts.map +1 -1
- package/dist/components/tables/cells/cell.text.d.ts +4 -0
- package/dist/components/tables/cells/cell.text.d.ts.map +1 -1
- package/dist/components/tables/index.d.ts +2 -0
- package/dist/components/tables/index.d.ts.map +1 -1
- package/dist/components/tables/sorting.d.ts +24 -0
- package/dist/components/tables/sorting.d.ts.map +1 -0
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/email-automations/index.js +120 -115
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +18 -13
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/analytics/index.js +114 -114
- package/dist/features/analytics/index.mjs +3 -3
- package/dist/features/analytics/server.js +4 -4
- package/dist/features/analytics/server.mjs +2 -2
- package/dist/features/billing/stripe-coupon/components/lists/CouponCodesList.d.ts.map +1 -1
- package/dist/features/billing/stripe-coupon/components/lists/CouponsList.d.ts.map +1 -1
- package/dist/features/billing/stripe-coupon/data/stripe-coupon-code.service.d.ts +2 -0
- package/dist/features/billing/stripe-coupon/data/stripe-coupon-code.service.d.ts.map +1 -1
- package/dist/features/billing/stripe-coupon/data/stripe-coupon.service.d.ts +1 -0
- package/dist/features/billing/stripe-coupon/data/stripe-coupon.service.d.ts.map +1 -1
- package/dist/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.d.ts.map +1 -1
- package/dist/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.d.ts.map +1 -1
- package/dist/features/billing/stripe-price/components/lists/PricesList.d.ts.map +1 -1
- package/dist/features/billing/stripe-price/data/stripe-price.service.d.ts +2 -0
- package/dist/features/billing/stripe-price/data/stripe-price.service.d.ts.map +1 -1
- package/dist/features/billing/stripe-price/hooks/useStripePriceTableStructure.d.ts.map +1 -1
- package/dist/features/billing/stripe-product/components/lists/ProductsList.d.ts.map +1 -1
- package/dist/features/billing/stripe-product/data/stripe-product.service.d.ts +2 -0
- package/dist/features/billing/stripe-product/data/stripe-product.service.d.ts.map +1 -1
- package/dist/features/billing/stripe-product/hooks/useStripeProductTableStructure.d.ts.map +1 -1
- package/dist/features/blog/components/admin/BlogPostsList.d.ts.map +1 -1
- package/dist/features/blog/data/BlogPostService.d.ts +1 -0
- package/dist/features/blog/data/BlogPostService.d.ts.map +1 -1
- package/dist/features/blog/index.js +134 -132
- package/dist/features/blog/index.js.map +1 -1
- package/dist/features/blog/index.mjs +8 -6
- package/dist/features/blog/index.mjs.map +1 -1
- package/dist/features/blog/server.js +7 -7
- package/dist/features/blog/server.mjs +1 -1
- package/dist/features/company/components/lists/CompaniesList.d.ts.map +1 -1
- package/dist/features/company/data/company.service.d.ts +1 -0
- package/dist/features/company/data/company.service.d.ts.map +1 -1
- package/dist/features/content/data/content.service.d.ts +3 -0
- package/dist/features/content/data/content.service.d.ts.map +1 -1
- package/dist/features/email-automation/components/lists/EmailAutomationRecipientsList.d.ts.map +1 -1
- package/dist/features/email-automation/components/lists/EmailAutomationsList.d.ts.map +1 -1
- package/dist/features/email-automation/data/EmailAutomationService.d.ts +3 -0
- package/dist/features/email-automation/data/EmailAutomationService.d.ts.map +1 -1
- package/dist/features/email-automation/hooks/useEmailAutomationTableStructure.d.ts.map +1 -1
- package/dist/features/email-blast/components/lists/EmailBlastsList.d.ts.map +1 -1
- package/dist/features/email-blast/data/EmailBlastService.d.ts +1 -0
- package/dist/features/email-blast/data/EmailBlastService.d.ts.map +1 -1
- package/dist/features/email-blast/hooks/useEmailBlastTableStructure.d.ts.map +1 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/how-to/components/lists/HowToList.d.ts.map +1 -1
- package/dist/features/how-to/data/HowToService.d.ts +2 -0
- package/dist/features/how-to/data/HowToService.d.ts.map +1 -1
- package/dist/features/notification/components/lists/NotificationsList.d.ts.map +1 -1
- package/dist/features/notification/data/notification.service.d.ts +2 -0
- package/dist/features/notification/data/notification.service.d.ts.map +1 -1
- package/dist/features/referral/components/containers/ReferralsAdminContainer.d.ts.map +1 -1
- package/dist/features/referral/data/ReferralAdminService.d.ts +1 -0
- package/dist/features/referral/data/ReferralAdminService.d.ts.map +1 -1
- package/dist/features/referral/hooks/useReferralAdminReferralTableStructure.d.ts.map +1 -1
- package/dist/features/role/components/lists/RolesList.d.ts.map +1 -1
- package/dist/features/role/components/lists/UserRolesList.d.ts.map +1 -1
- package/dist/features/role/data/role.service.d.ts +3 -0
- package/dist/features/role/data/role.service.d.ts.map +1 -1
- package/dist/features/tokenusage/index.js +114 -114
- package/dist/features/tokenusage/index.mjs +3 -3
- package/dist/features/user/components/lists/AdminUsersList.d.ts.map +1 -1
- package/dist/features/user/components/lists/CompanyUsersList.d.ts.map +1 -1
- package/dist/features/user/components/lists/PlatformUsersList.d.ts.map +1 -1
- package/dist/features/user/components/lists/RoleUsersList.d.ts.map +1 -1
- package/dist/features/user/components/lists/UsersListByContentIds.d.ts.map +1 -1
- package/dist/features/user/data/user.service.d.ts +11 -0
- package/dist/features/user/data/user.service.d.ts.map +1 -1
- package/dist/features/waitlist/components/lists/WaitlistList.d.ts.map +1 -1
- package/dist/features/waitlist/data/WaitlistService.d.ts +2 -0
- package/dist/features/waitlist/data/WaitlistService.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useDataListRetriever.d.ts +13 -0
- package/dist/hooks/useDataListRetriever.d.ts.map +1 -1
- package/dist/hooks/useListSortParam.d.ts +23 -0
- package/dist/hooks/useListSortParam.d.ts.map +1 -0
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/dist/shadcnui/ui/calendar.d.ts +1 -1
- package/dist/shadcnui/ui/calendar.d.ts.map +1 -1
- package/messages/en.json +11 -0
- package/messages/it.json +11 -0
- package/package.json +1 -1
- package/src/components/tables/ContentListTable.tsx +108 -10
- package/src/components/tables/SortableHeader.tsx +43 -0
- package/src/components/tables/__tests__/ContentListTable.rendering.spec.tsx +1 -0
- package/src/components/tables/__tests__/ContentListTable.sort.spec.tsx +273 -0
- package/src/components/tables/__tests__/sorting.spec.ts +61 -0
- package/src/components/tables/cells/cell.date.tsx +10 -2
- package/src/components/tables/cells/cell.dateTime.tsx +10 -2
- package/src/components/tables/cells/cell.link.tsx +6 -1
- package/src/components/tables/cells/cell.text.tsx +11 -2
- package/src/components/tables/index.ts +2 -0
- package/src/components/tables/sorting.ts +41 -0
- package/src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx +2 -0
- package/src/features/billing/stripe-coupon/components/lists/CouponsList.tsx +2 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.service.ts +4 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +7 -1
- package/src/features/billing/stripe-coupon/hooks/__tests__/useStripeCouponCodeTableStructure.sort.spec.tsx +30 -0
- package/src/features/billing/stripe-coupon/hooks/__tests__/useStripeCouponTableStructure.sort.spec.tsx +33 -0
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +5 -4
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +7 -4
- package/src/features/billing/stripe-price/components/lists/PricesList.tsx +2 -0
- package/src/features/billing/stripe-price/data/stripe-price.service.ts +4 -0
- package/src/features/billing/stripe-price/hooks/__tests__/useStripePriceTableStructure.sort.spec.tsx +29 -0
- package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +8 -5
- package/src/features/billing/stripe-product/components/lists/ProductsList.tsx +2 -0
- package/src/features/billing/stripe-product/data/stripe-product.service.ts +4 -0
- package/src/features/billing/stripe-product/hooks/__tests__/useStripeProductTableStructure.sort.spec.tsx +29 -0
- package/src/features/billing/stripe-product/hooks/useStripeProductTableStructure.tsx +3 -2
- package/src/features/blog/components/admin/BlogPostsList.tsx +2 -0
- package/src/features/blog/data/BlogPostService.ts +3 -1
- package/src/features/blog/hooks/__tests__/useBlogPostTableStructure.sort.spec.tsx +19 -0
- package/src/features/blog/hooks/useBlogPostTableStructure.tsx +2 -2
- package/src/features/company/components/lists/CompaniesList.tsx +2 -0
- package/src/features/company/data/company.service.ts +3 -1
- package/src/features/company/hooks/__tests__/useCompanyTableStructure.sort.spec.tsx +19 -0
- package/src/features/company/hooks/useCompanyTableStructure.tsx +1 -1
- package/src/features/content/data/content.service.ts +12 -1
- package/src/features/content/hooks/__tests__/useContentTableStructure.sort.spec.tsx +21 -0
- package/src/features/content/hooks/useContentTableStructure.tsx +1 -1
- package/src/features/email-automation/components/lists/EmailAutomationRecipientsList.tsx +2 -0
- package/src/features/email-automation/components/lists/EmailAutomationsList.tsx +2 -0
- package/src/features/email-automation/data/EmailAutomationService.ts +9 -1
- package/src/features/email-automation/hooks/__tests__/useEmailAutomationEnrollmentTableStructure.sort.spec.tsx +21 -0
- package/src/features/email-automation/hooks/__tests__/useEmailAutomationTableStructure.sort.spec.tsx +29 -0
- package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +4 -4
- package/src/features/email-automation/hooks/useEmailAutomationTableStructure.tsx +5 -4
- package/src/features/email-blast/components/lists/EmailBlastsList.tsx +2 -0
- package/src/features/email-blast/data/EmailBlastService.ts +5 -1
- package/src/features/email-blast/hooks/__tests__/useEmailBlastTableStructure.sort.spec.tsx +29 -0
- package/src/features/email-blast/hooks/useEmailBlastTableStructure.tsx +16 -8
- package/src/features/how-to/components/lists/HowToList.tsx +2 -0
- package/src/features/how-to/data/HowToService.ts +4 -0
- package/src/features/how-to/hooks/__tests__/useHowToTableStructure.sort.spec.tsx +18 -0
- package/src/features/how-to/hooks/useHowToTableStructure.tsx +1 -1
- package/src/features/notification/components/lists/NotificationsList.tsx +2 -0
- package/src/features/notification/data/notification.service.ts +4 -0
- package/src/features/notification/hooks/__tests__/useNotificationTableStructure.sort.spec.tsx +19 -0
- package/src/features/notification/hooks/useNotificationTableStructure.tsx +1 -1
- package/src/features/referral/components/containers/ReferralsAdminContainer.tsx +4 -2
- package/src/features/referral/data/ReferralAdminService.ts +6 -1
- package/src/features/referral/hooks/__tests__/useReferralAdminReferralTableStructure.sort.spec.tsx +35 -0
- package/src/features/referral/hooks/useReferralAdminReferralTableStructure.tsx +10 -5
- package/src/features/role/components/lists/RolesList.tsx +2 -0
- package/src/features/role/components/lists/UserRolesList.tsx +2 -0
- package/src/features/role/data/role.service.ts +7 -1
- package/src/features/role/hooks/__tests__/useRoleTableStructure.sort.spec.tsx +16 -0
- package/src/features/role/hooks/useRoleTableStructure.tsx +2 -2
- package/src/features/user/components/lists/AdminUsersList.tsx +2 -0
- package/src/features/user/components/lists/CompanyUsersList.tsx +4 -0
- package/src/features/user/components/lists/PlatformUsersList.tsx +2 -0
- package/src/features/user/components/lists/RoleUsersList.tsx +2 -0
- package/src/features/user/components/lists/UsersListByContentIds.tsx +2 -0
- package/src/features/user/data/user.service.ts +28 -1
- package/src/features/user/hooks/__tests__/useUserTableStructure.sort.spec.tsx +16 -0
- package/src/features/user/hooks/useUserTableStructure.tsx +2 -2
- package/src/features/waitlist/components/lists/WaitlistList.tsx +2 -0
- package/src/features/waitlist/data/WaitlistService.ts +4 -0
- package/src/features/waitlist/hooks/__tests__/useWaitlistTableStructure.sort.spec.tsx +17 -0
- package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +3 -3
- package/src/hooks/__tests__/useDataListRetriever.sort.spec.tsx +114 -0
- package/src/hooks/__tests__/useListSortParam.spec.tsx +91 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useDataListRetriever.ts +44 -4
- package/src/hooks/useListSortParam.ts +53 -0
- package/src/shadcnui/ui/__tests__/calendar.spec.tsx +222 -0
- package/src/shadcnui/ui/calendar.tsx +28 -1
- package/dist/chunk-4JVLLBRS.mjs.map +0 -1
- package/dist/chunk-F25BVBEH.mjs.map +0 -1
- package/dist/chunk-NPGX2GJD.js.map +0 -1
- package/dist/chunk-SQNFYMYE.js.map +0 -1
- /package/dist/{BlockNoteEditor-XIEMUQCO.mjs.map → BlockNoteEditor-DCVT7YHE.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-NVSR67X5.mjs.map → BlockNoteViewer-XDAC6AL7.mjs.map} +0 -0
- /package/dist/{chunk-AEBZ67JJ.mjs.map → chunk-ETU3AW2X.mjs.map} +0 -0
- /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: {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
});
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
+
}
|