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