@carlonicora/nextjs-jsonapi 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/{BlockNoteEditor-EGY4ZAAM.js → BlockNoteEditor-UJ6363LC.js} +19 -19
  2. package/dist/{BlockNoteEditor-EGY4ZAAM.js.map → BlockNoteEditor-UJ6363LC.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-2QJVK2HH.mjs → BlockNoteEditor-WTSQANKI.mjs} +4 -4
  4. package/dist/{BlockNoteViewer-I2MMFH3S.js → BlockNoteViewer-EL4JFB6P.js} +3 -3
  5. package/dist/{BlockNoteViewer-I2MMFH3S.js.map → BlockNoteViewer-EL4JFB6P.js.map} +1 -1
  6. package/dist/{BlockNoteViewer-6BQXJ4TB.mjs → BlockNoteViewer-JVKWJZ6J.mjs} +2 -2
  7. package/dist/billing/index.js +441 -441
  8. package/dist/billing/index.mjs +3 -3
  9. package/dist/{chunk-3VEQ5JZJ.js → chunk-3BAM23VA.js} +847 -847
  10. package/dist/chunk-3BAM23VA.js.map +1 -0
  11. package/dist/{chunk-JEZQTZQ4.mjs → chunk-6WU74VFU.mjs} +2 -2
  12. package/dist/{chunk-S5MQDWWC.js → chunk-CRI4T47Y.js} +736 -176
  13. package/dist/chunk-CRI4T47Y.js.map +1 -0
  14. package/dist/{chunk-75MLT74B.js → chunk-EO75CV76.js} +8 -8
  15. package/dist/{chunk-75MLT74B.js.map → chunk-EO75CV76.js.map} +1 -1
  16. package/dist/{chunk-NPKFMRH5.mjs → chunk-FNTQQFLH.mjs} +2 -2
  17. package/dist/{chunk-HNOWSFM5.mjs → chunk-JQWI62NV.mjs} +563 -3
  18. package/dist/chunk-JQWI62NV.mjs.map +1 -0
  19. package/dist/{chunk-2Z3ZCYXX.mjs → chunk-MIZTWBPX.mjs} +5 -5
  20. package/dist/chunk-MIZTWBPX.mjs.map +1 -0
  21. package/dist/{chunk-YMUPTXOT.mjs → chunk-QKZUE5OF.mjs} +2 -2
  22. package/dist/{chunk-EXICQC4A.js → chunk-QQQE3N2X.js} +3 -3
  23. package/dist/{chunk-EXICQC4A.js.map → chunk-QQQE3N2X.js.map} +1 -1
  24. package/dist/{chunk-THKYH65N.js → chunk-XGTQWBDJ.js} +4 -4
  25. package/dist/{chunk-THKYH65N.js.map → chunk-XGTQWBDJ.js.map} +1 -1
  26. package/dist/client/index.d.mts +6 -39
  27. package/dist/client/index.d.ts +6 -39
  28. package/dist/client/index.js +4 -4
  29. package/dist/client/index.mjs +3 -3
  30. package/dist/components/index.d.mts +2 -2
  31. package/dist/components/index.d.ts +2 -2
  32. package/dist/components/index.js +4 -4
  33. package/dist/components/index.mjs +3 -3
  34. package/dist/contexts/index.js +4 -4
  35. package/dist/contexts/index.mjs +3 -3
  36. package/dist/core/index.d.mts +187 -1
  37. package/dist/core/index.d.ts +187 -1
  38. package/dist/core/index.js +30 -2
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/index.mjs +29 -1
  41. package/dist/email-automation-enrollment.fields-BZvF14qx.d.mts +82 -0
  42. package/dist/email-automation-enrollment.fields-Cp3VMzFq.d.ts +82 -0
  43. package/dist/email-automations/index.css +30 -0
  44. package/dist/email-automations/index.css.map +1 -0
  45. package/dist/email-automations/index.d.mts +303 -0
  46. package/dist/email-automations/index.d.ts +303 -0
  47. package/dist/email-automations/index.js +1705 -0
  48. package/dist/email-automations/index.js.map +1 -0
  49. package/dist/email-automations/index.mjs +1705 -0
  50. package/dist/email-automations/index.mjs.map +1 -0
  51. package/dist/features/analytics/index.js +122 -122
  52. package/dist/features/analytics/index.mjs +4 -4
  53. package/dist/features/analytics/server.js +4 -4
  54. package/dist/features/analytics/server.mjs +2 -2
  55. package/dist/features/help/index.js +37 -37
  56. package/dist/features/help/index.mjs +3 -3
  57. package/dist/features/tokenusage/index.js +125 -125
  58. package/dist/features/tokenusage/index.mjs +4 -4
  59. package/dist/index.d.mts +2 -1
  60. package/dist/index.d.ts +2 -1
  61. package/dist/index.js +31 -3
  62. package/dist/index.js.map +1 -1
  63. package/dist/index.mjs +30 -2
  64. package/dist/server/index.js +3 -3
  65. package/dist/server/index.mjs +2 -2
  66. package/dist/types-70acd-8C.d.mts +39 -0
  67. package/dist/types-l8SFleXi.d.ts +39 -0
  68. package/messages/en.json +117 -0
  69. package/messages/it.json +117 -0
  70. package/package.json +11 -1
  71. package/src/components/forms/FormFieldWrapper.tsx +1 -1
  72. package/src/components/forms/FormTextarea.tsx +2 -1
  73. package/src/core/index.ts +8 -0
  74. package/src/core/registry/ModuleRegistry.ts +5 -0
  75. package/src/email-automations/index.ts +5 -0
  76. package/src/features/email-automation/EmailAutomationEnrollmentModule.ts +11 -0
  77. package/src/features/email-automation/EmailAutomationModule.ts +30 -0
  78. package/src/features/email-automation/EmailAutomationStepModule.ts +11 -0
  79. package/src/features/email-automation/EmailAutomationTriggerModule.ts +11 -0
  80. package/src/features/email-automation/components/__tests__/EmailAutomationEditor.spec.tsx +104 -0
  81. package/src/features/email-automation/components/canvas/EmailAutomationActiveSwitch.tsx +49 -0
  82. package/src/features/email-automation/components/canvas/EmailAutomationCanvas.tsx +474 -0
  83. package/src/features/email-automation/components/canvas/EmailAutomationCanvasInspector.tsx +89 -0
  84. package/src/features/email-automation/components/canvas/EmailAutomationCanvasTab.tsx +32 -0
  85. package/src/features/email-automation/components/canvas/__tests__/buildCanvasGraph.spec.ts +67 -0
  86. package/src/features/email-automation/components/canvas/buildCanvasGraph.ts +141 -0
  87. package/src/features/email-automation/components/canvas/edges/DelayEdge.tsx +68 -0
  88. package/src/features/email-automation/components/canvas/nodes/EmailStepNode.tsx +43 -0
  89. package/src/features/email-automation/components/canvas/nodes/TriggerNode.tsx +23 -0
  90. package/src/features/email-automation/components/canvas/shared/CanvasToolbar.tsx +53 -0
  91. package/src/features/email-automation/components/canvas/shared/EmptyCanvas.tsx +11 -0
  92. package/src/features/email-automation/components/canvas/shared/__tests__/autoLayout.spec.ts +42 -0
  93. package/src/features/email-automation/components/canvas/shared/__tests__/longPress.spec.ts +51 -0
  94. package/src/features/email-automation/components/canvas/shared/autoLayout.ts +55 -0
  95. package/src/features/email-automation/components/canvas/shared/index.ts +4 -0
  96. package/src/features/email-automation/components/canvas/shared/longPress.ts +57 -0
  97. package/src/features/email-automation/components/containers/EmailAutomationContainer.tsx +80 -0
  98. package/src/features/email-automation/components/containers/EmailAutomationsListContainer.tsx +27 -0
  99. package/src/features/email-automation/components/forms/EmailAutomationEditor.tsx +153 -0
  100. package/src/features/email-automation/components/forms/EmailStepEditor.tsx +151 -0
  101. package/src/features/email-automation/components/index.ts +15 -0
  102. package/src/features/email-automation/components/lists/EmailAutomationRecipientsList.tsx +90 -0
  103. package/src/features/email-automation/components/lists/EmailAutomationsList.tsx +76 -0
  104. package/src/features/email-automation/components/unsubscribe/EmailUnsubscribe.tsx +99 -0
  105. package/src/features/email-automation/components/unsubscribe/__tests__/EmailUnsubscribe.spec.tsx +55 -0
  106. package/src/features/email-automation/contexts/EmailAutomationContext.tsx +175 -0
  107. package/src/features/email-automation/data/EmailAutomation.ts +87 -0
  108. package/src/features/email-automation/data/EmailAutomationEnrollment.ts +105 -0
  109. package/src/features/email-automation/data/EmailAutomationEnrollmentInterface.ts +18 -0
  110. package/src/features/email-automation/data/EmailAutomationEnrollmentService.ts +31 -0
  111. package/src/features/email-automation/data/EmailAutomationInterface.ts +22 -0
  112. package/src/features/email-automation/data/EmailAutomationService.ts +106 -0
  113. package/src/features/email-automation/data/EmailAutomationStep.ts +104 -0
  114. package/src/features/email-automation/data/EmailAutomationStepInterface.ts +23 -0
  115. package/src/features/email-automation/data/EmailAutomationStepService.ts +80 -0
  116. package/src/features/email-automation/data/EmailAutomationTrigger.ts +28 -0
  117. package/src/features/email-automation/data/EmailAutomationTriggerInterface.ts +6 -0
  118. package/src/features/email-automation/data/__tests__/email-automation-models.spec.ts +103 -0
  119. package/src/features/email-automation/data/email-automation-enrollment.fields.ts +8 -0
  120. package/src/features/email-automation/data/email-automation.fields.ts +9 -0
  121. package/src/features/email-automation/data/index.ts +13 -0
  122. package/src/features/email-automation/hooks/index.ts +2 -0
  123. package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +126 -0
  124. package/src/features/email-automation/hooks/useEmailAutomationTableStructure.tsx +119 -0
  125. package/src/features/email-automation/index.ts +5 -0
  126. package/dist/chunk-2Z3ZCYXX.mjs.map +0 -1
  127. package/dist/chunk-3VEQ5JZJ.js.map +0 -1
  128. package/dist/chunk-HNOWSFM5.mjs.map +0 -1
  129. package/dist/chunk-S5MQDWWC.js.map +0 -1
  130. /package/dist/{BlockNoteEditor-2QJVK2HH.mjs.map → BlockNoteEditor-WTSQANKI.mjs.map} +0 -0
  131. /package/dist/{BlockNoteViewer-6BQXJ4TB.mjs.map → BlockNoteViewer-JVKWJZ6J.mjs.map} +0 -0
  132. /package/dist/{chunk-JEZQTZQ4.mjs.map → chunk-6WU74VFU.mjs.map} +0 -0
  133. /package/dist/{chunk-NPKFMRH5.mjs.map → chunk-FNTQQFLH.mjs.map} +0 -0
  134. /package/dist/{chunk-YMUPTXOT.mjs.map → chunk-QKZUE5OF.mjs.map} +0 -0
@@ -0,0 +1,80 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import { EmailAutomationStep } from "./EmailAutomationStep";
3
+ import { EmailAutomationStepInput, EmailAutomationStepInterface } from "./EmailAutomationStepInterface";
4
+
5
+ export class EmailAutomationStepService extends AbstractService {
6
+ static async findByAutomation(params: { automationId: string }): Promise<EmailAutomationStepInterface[]> {
7
+ const endpoint = new EndpointCreator({
8
+ endpoint: Modules.EmailAutomation,
9
+ id: params.automationId,
10
+ childEndpoint: Modules.EmailAutomationStep,
11
+ });
12
+
13
+ endpoint.addAdditionalParam("fetchAll", "true");
14
+
15
+ return this.callApi<EmailAutomationStepInterface[]>({
16
+ type: Modules.EmailAutomationStep,
17
+ method: HttpMethod.GET,
18
+ endpoint: endpoint.generate(),
19
+ });
20
+ }
21
+
22
+ static async create(params: EmailAutomationStepInput): Promise<EmailAutomationStepInterface> {
23
+ return this.callApi<EmailAutomationStepInterface>({
24
+ type: Modules.EmailAutomationStep,
25
+ method: HttpMethod.POST,
26
+ endpoint: new EndpointCreator({ endpoint: Modules.EmailAutomationStep }).generate(),
27
+ input: params,
28
+ });
29
+ }
30
+
31
+ static async update(params: EmailAutomationStepInput): Promise<EmailAutomationStepInterface> {
32
+ return this.callApi<EmailAutomationStepInterface>({
33
+ type: Modules.EmailAutomationStep,
34
+ method: HttpMethod.PATCH,
35
+ endpoint: new EndpointCreator({ endpoint: Modules.EmailAutomationStep, id: params.id }).generate(),
36
+ input: params,
37
+ });
38
+ }
39
+
40
+ static async delete(params: { id: string }): Promise<void> {
41
+ await this.callApi({
42
+ type: Modules.EmailAutomationStep,
43
+ method: HttpMethod.DELETE,
44
+ endpoint: new EndpointCreator({ endpoint: Modules.EmailAutomationStep, id: params.id }).generate(),
45
+ });
46
+ }
47
+
48
+ /**
49
+ * POST email-automation-steps/:stepId/next: continues the chain from stepId to nextStepId.
50
+ */
51
+ static async setNext(params: { stepId: string; nextStepId: string }): Promise<EmailAutomationStepInterface> {
52
+ const step = new EmailAutomationStep();
53
+ return this.callApi<EmailAutomationStepInterface>({
54
+ type: Modules.EmailAutomationStep,
55
+ method: HttpMethod.POST,
56
+ endpoint: new EndpointCreator({
57
+ endpoint: Modules.EmailAutomationStep,
58
+ id: params.stepId,
59
+ childEndpoint: "next",
60
+ }).generate(),
61
+ input: step.createNextJsonApi({ nextStepId: params.nextStepId }),
62
+ overridesJsonApiCreation: true,
63
+ });
64
+ }
65
+
66
+ /**
67
+ * DELETE email-automation-steps/:stepId/next: the step becomes the end of the chain.
68
+ */
69
+ static async clearNext(params: { stepId: string }): Promise<void> {
70
+ await this.callApi({
71
+ type: Modules.EmailAutomationStep,
72
+ method: HttpMethod.DELETE,
73
+ endpoint: new EndpointCreator({
74
+ endpoint: Modules.EmailAutomationStep,
75
+ id: params.stepId,
76
+ childEndpoint: "next",
77
+ }).generate(),
78
+ });
79
+ }
80
+ }
@@ -0,0 +1,28 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface } from "../../../core";
2
+ import { EmailAutomationTriggerInterface } from "./EmailAutomationTriggerInterface";
3
+
4
+ export class EmailAutomationTrigger extends AbstractApiData implements EmailAutomationTriggerInterface {
5
+ private _key?: string;
6
+ private _label?: string;
7
+
8
+ get key(): string {
9
+ return this._key ?? "";
10
+ }
11
+
12
+ get label(): string {
13
+ return this._label ?? "";
14
+ }
15
+
16
+ rehydrate(data: JsonApiHydratedDataInterface): this {
17
+ super.rehydrate(data);
18
+
19
+ this._key = data.jsonApi.attributes.key;
20
+ this._label = data.jsonApi.attributes.label;
21
+
22
+ return this;
23
+ }
24
+
25
+ createJsonApi(_data?: any): any {
26
+ throw new Error("EmailAutomationTrigger is declared by the API and is read-only");
27
+ }
28
+ }
@@ -0,0 +1,6 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ export interface EmailAutomationTriggerInterface extends ApiDataInterface {
4
+ get key(): string;
5
+ get label(): string;
6
+ }
@@ -0,0 +1,103 @@
1
+ import { beforeAll, describe, expect, it } from "vitest";
2
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
3
+ import { ModuleFactory, ModuleWithPermissions } from "../../../../permissions/types";
4
+ import { EmailAutomationEnrollmentModule } from "../../EmailAutomationEnrollmentModule";
5
+ import { EmailAutomationModule } from "../../EmailAutomationModule";
6
+ import { EmailAutomationStepModule } from "../../EmailAutomationStepModule";
7
+ import { EmailAutomationTriggerModule } from "../../EmailAutomationTriggerModule";
8
+ import { EmailAutomation } from "../EmailAutomation";
9
+ import { EMAIL_AUTOMATION_UNSUBSCRIBE_TYPE, EmailAutomationEnrollment } from "../EmailAutomationEnrollment";
10
+ import { EmailAutomationStep } from "../EmailAutomationStep";
11
+
12
+ // createJsonApi reads Modules.X.name (resolved lazily via ModuleRegistry), so
13
+ // the modules must be registered before a model serialises — mirrors the
14
+ // tokenusage-admin-models.spec setup convention in this package.
15
+ const moduleFactory: ModuleFactory = (params) =>
16
+ ({
17
+ pageUrl: params.pageUrl,
18
+ name: params.name,
19
+ model: params.model,
20
+ }) as ModuleWithPermissions;
21
+
22
+ beforeAll(() => {
23
+ ModuleRegistry.register("EmailAutomation", EmailAutomationModule(moduleFactory));
24
+ ModuleRegistry.register("EmailAutomationStep", EmailAutomationStepModule(moduleFactory));
25
+ ModuleRegistry.register("EmailAutomationEnrollment", EmailAutomationEnrollmentModule(moduleFactory));
26
+ ModuleRegistry.register("EmailAutomationTrigger", EmailAutomationTriggerModule(moduleFactory));
27
+ });
28
+
29
+ const hydrate = (attributes: Record<string, unknown>) =>
30
+ ({ jsonApi: { id: "x", type: "t", attributes }, included: [] }) as any;
31
+
32
+ describe("email automation models", () => {
33
+ it("parses the enrollment datetimes into Date, not the wire string", () => {
34
+ const m = new EmailAutomationEnrollment().rehydrate(
35
+ hydrate({
36
+ email: "ada@example.com",
37
+ status: "active",
38
+ enrolledAt: "2026-10-21T08:00:00.000Z",
39
+ nextSendAt: "2026-10-22T08:00:00.000Z",
40
+ attempts: 0,
41
+ }),
42
+ );
43
+ expect(m.enrolledAt).toBeInstanceOf(Date);
44
+ expect(m.enrolledAt.toISOString()).toBe("2026-10-21T08:00:00.000Z");
45
+ expect(m.nextSendAt).toBeInstanceOf(Date);
46
+ expect(m.nextSendAt?.toISOString()).toBe("2026-10-22T08:00:00.000Z");
47
+ expect(m.finishedAt).toBeUndefined();
48
+ });
49
+
50
+ it("reads the enrollment computed fields from attributes", () => {
51
+ const m = new EmailAutomationEnrollment().rehydrate(
52
+ hydrate({
53
+ email: "ada@example.com",
54
+ status: "active",
55
+ enrolledAt: "2026-10-21T08:00:00.000Z",
56
+ currentStepSubject: "Welcome aboard",
57
+ sentCount: 2,
58
+ }),
59
+ );
60
+ expect(m.currentStepSubject).toBe("Welcome aboard");
61
+ expect(m.sentCount).toBe(2);
62
+ });
63
+
64
+ it("omits undefined automation attributes and includes isActive when given", () => {
65
+ const body = new EmailAutomation().createJsonApi({ id: "a1", name: "Onboarding", isActive: true });
66
+ expect(body.data.type).toBe("email-automations");
67
+ expect(body.data.id).toBe("a1");
68
+ expect(body.data.attributes).toEqual({ name: "Onboarding", isActive: true });
69
+ expect("trigger" in body.data.attributes).toBe(false);
70
+ expect("exitTrigger" in body.data.attributes).toBe(false);
71
+ expect("replyTo" in body.data.attributes).toBe(false);
72
+ });
73
+
74
+ it("includes isActive false when given", () => {
75
+ const body = new EmailAutomation().createJsonApi({ id: "a1", name: "Onboarding", isActive: false });
76
+ expect(body.data.attributes.isActive).toBe(false);
77
+ });
78
+
79
+ it("writes the step's automation relationship", () => {
80
+ const body = new EmailAutomationStep().createJsonApi({
81
+ id: "s1",
82
+ automationId: "a1",
83
+ subject: "Hello",
84
+ body: "Hi {{name}}",
85
+ delayHours: 24,
86
+ });
87
+ expect(body.data.type).toBe("email-automation-steps");
88
+ expect(body.data.attributes).toEqual({ subject: "Hello", body: "Hi {{name}}", delayHours: 24 });
89
+ expect(body.data.relationships.automation).toEqual({ data: { type: "email-automations", id: "a1" } });
90
+ });
91
+
92
+ it("builds the next-step body", () => {
93
+ expect(new EmailAutomationStep().createNextJsonApi({ nextStepId: "s2" })).toEqual({
94
+ data: { type: "email-automation-steps", id: "s2" },
95
+ });
96
+ });
97
+
98
+ it("builds the unsubscribe body", () => {
99
+ expect(new EmailAutomationEnrollment().createUnsubscribeJsonApi({ scope: "all" })).toEqual({
100
+ data: { type: EMAIL_AUTOMATION_UNSUBSCRIBE_TYPE, attributes: { scope: "all" } },
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,8 @@
1
+ export enum EmailAutomationEnrollmentFields {
2
+ email = "email",
3
+ name = "name",
4
+ status = "status",
5
+ currentStep = "currentStep",
6
+ nextSendAt = "nextSendAt",
7
+ sentCount = "sentCount",
8
+ }
@@ -0,0 +1,9 @@
1
+ export enum EmailAutomationFields {
2
+ name = "name",
3
+ trigger = "trigger",
4
+ status = "status",
5
+ steps = "steps",
6
+ active = "active",
7
+ completed = "completed",
8
+ unsubscribed = "unsubscribed",
9
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./EmailAutomationInterface";
2
+ export * from "./EmailAutomation";
3
+ export * from "./EmailAutomationService";
4
+ export * from "./EmailAutomationStepInterface";
5
+ export * from "./EmailAutomationStep";
6
+ export * from "./EmailAutomationStepService";
7
+ export * from "./EmailAutomationEnrollmentInterface";
8
+ export * from "./EmailAutomationEnrollment";
9
+ export * from "./EmailAutomationEnrollmentService";
10
+ export * from "./EmailAutomationTriggerInterface";
11
+ export * from "./EmailAutomationTrigger";
12
+ export * from "./email-automation.fields";
13
+ export * from "./email-automation-enrollment.fields";
@@ -0,0 +1,2 @@
1
+ export * from "./useEmailAutomationTableStructure";
2
+ export * from "./useEmailAutomationEnrollmentTableStructure";
@@ -0,0 +1,126 @@
1
+ "use client";
2
+
3
+ import { ColumnDef } from "@tanstack/react-table";
4
+ import { useLocale, useTranslations } from "next-intl";
5
+ import { useMemo } from "react";
6
+ import { Badge } from "../../../shadcnui";
7
+ import { registerTableGenerator, TableContent, UseTableStructureHook } from "../../../hooks";
8
+ import { dateFormatLocale, formatDate } from "../../../utils/date-formatter";
9
+ import { EmailAutomationEnrollmentFields } from "../data/email-automation-enrollment.fields";
10
+ import { EmailAutomationEnrollmentInterface } from "../data/EmailAutomationEnrollmentInterface";
11
+
12
+ const STATUS_VARIANTS: Record<string, "softGreen" | "softBlue" | "softGray" | "softRed"> = {
13
+ active: "softGreen",
14
+ completed: "softBlue",
15
+ unsubscribed: "softGray",
16
+ exited: "softGray",
17
+ failed: "softRed",
18
+ };
19
+
20
+ export const useEmailAutomationEnrollmentTableStructure: UseTableStructureHook<
21
+ EmailAutomationEnrollmentInterface,
22
+ EmailAutomationEnrollmentFields
23
+ > = (params) => {
24
+ const t = useTranslations();
25
+ const locale = dateFormatLocale(useLocale());
26
+
27
+ const tableData = useMemo(() => {
28
+ return params.data.map((enrollment: EmailAutomationEnrollmentInterface) => {
29
+ const entry: TableContent<EmailAutomationEnrollmentInterface> = { jsonApiData: enrollment };
30
+ params.fields.forEach((field) => {
31
+ entry[field] = (enrollment as any)[field as keyof EmailAutomationEnrollmentInterface];
32
+ });
33
+ return entry;
34
+ });
35
+ }, [params.data, params.fields]);
36
+
37
+ const empty = <span className="text-muted-foreground text-xs">—</span>;
38
+
39
+ const fieldColumnMap: Partial<Record<EmailAutomationEnrollmentFields, () => any>> = {
40
+ [EmailAutomationEnrollmentFields.email]: () => ({
41
+ id: "email",
42
+ accessorKey: "email",
43
+ header: t("emailAutomations.recipients.columns.email"),
44
+ cell: ({ row }: { row: TableContent<EmailAutomationEnrollmentInterface> }) => {
45
+ const enrollment: EmailAutomationEnrollmentInterface = row.original.jsonApiData;
46
+ return <span className="text-sm">{enrollment.email}</span>;
47
+ },
48
+ enableSorting: false,
49
+ enableHiding: false,
50
+ }),
51
+ [EmailAutomationEnrollmentFields.name]: () => ({
52
+ id: "name",
53
+ accessorKey: "name",
54
+ header: t("emailAutomations.recipients.columns.name"),
55
+ cell: ({ row }: { row: TableContent<EmailAutomationEnrollmentInterface> }) => {
56
+ const enrollment: EmailAutomationEnrollmentInterface = row.original.jsonApiData;
57
+ return enrollment.name ? <span className="text-sm">{enrollment.name}</span> : empty;
58
+ },
59
+ enableSorting: false,
60
+ enableHiding: false,
61
+ }),
62
+ [EmailAutomationEnrollmentFields.status]: () => ({
63
+ id: "status",
64
+ accessorKey: "status",
65
+ header: t("emailAutomations.recipients.columns.status"),
66
+ cell: ({ row }: { row: TableContent<EmailAutomationEnrollmentInterface> }) => {
67
+ const enrollment: EmailAutomationEnrollmentInterface = row.original.jsonApiData;
68
+ return (
69
+ <Badge
70
+ variant={STATUS_VARIANTS[enrollment.status] ?? "softGray"}
71
+ title={enrollment.status === "failed" ? enrollment.lastError : undefined}
72
+ >
73
+ {t(`emailAutomations.recipients.status.${enrollment.status}`)}
74
+ </Badge>
75
+ );
76
+ },
77
+ enableSorting: false,
78
+ enableHiding: false,
79
+ }),
80
+ [EmailAutomationEnrollmentFields.currentStep]: () => ({
81
+ id: "currentStep",
82
+ accessorKey: "currentStep",
83
+ header: t("emailAutomations.recipients.columns.currentStep"),
84
+ cell: ({ row }: { row: TableContent<EmailAutomationEnrollmentInterface> }) => {
85
+ const enrollment: EmailAutomationEnrollmentInterface = row.original.jsonApiData;
86
+ return enrollment.currentStepSubject ? <span className="text-sm">{enrollment.currentStepSubject}</span> : empty;
87
+ },
88
+ enableSorting: false,
89
+ enableHiding: false,
90
+ }),
91
+ [EmailAutomationEnrollmentFields.nextSendAt]: () => ({
92
+ id: "nextSendAt",
93
+ accessorKey: "nextSendAt",
94
+ header: t("emailAutomations.recipients.columns.nextSendAt"),
95
+ cell: ({ row }: { row: TableContent<EmailAutomationEnrollmentInterface> }) => {
96
+ const enrollment: EmailAutomationEnrollmentInterface = row.original.jsonApiData;
97
+ if (!enrollment.nextSendAt) return empty;
98
+ return <span className="text-xs tabular-nums">{formatDate(enrollment.nextSendAt, "dateTime", locale)}</span>;
99
+ },
100
+ enableSorting: false,
101
+ enableHiding: false,
102
+ }),
103
+ [EmailAutomationEnrollmentFields.sentCount]: () => ({
104
+ id: "sentCount",
105
+ accessorKey: "sentCount",
106
+ header: t("emailAutomations.recipients.columns.sentCount"),
107
+ cell: ({ row }: { row: TableContent<EmailAutomationEnrollmentInterface> }) => {
108
+ const enrollment: EmailAutomationEnrollmentInterface = row.original.jsonApiData;
109
+ // Typography role 15 (numeric): tabular-nums, end-aligned.
110
+ return <span className="block text-xs tabular-nums text-end">{enrollment.sentCount ?? 0}</span>;
111
+ },
112
+ enableSorting: false,
113
+ enableHiding: false,
114
+ }),
115
+ };
116
+
117
+ const columns = useMemo(() => {
118
+ return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
119
+ TableContent<EmailAutomationEnrollmentInterface>
120
+ >[];
121
+ }, [params.fields, fieldColumnMap, t, locale]);
122
+
123
+ return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
124
+ };
125
+
126
+ registerTableGenerator("email-automation-enrollments", useEmailAutomationEnrollmentTableStructure);
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ import { ColumnDef } from "@tanstack/react-table";
4
+ import { useTranslations } from "next-intl";
5
+ import { useMemo } from "react";
6
+ import { Badge, Link } from "../../../shadcnui";
7
+ import { Modules } from "../../../core";
8
+ import { registerTableGenerator, TableContent, usePageUrlGenerator, UseTableStructureHook } from "../../../hooks";
9
+ import { EmailAutomationFields } from "../data/email-automation.fields";
10
+ import { EmailAutomationInterface } from "../data/EmailAutomationInterface";
11
+
12
+ /**
13
+ * `context.triggerLabels` maps a trigger key to the label the API declares for
14
+ * it (EmailAutomationsList loads them). A key the app no longer declares falls
15
+ * back to the raw key, so a stale automation stays readable.
16
+ */
17
+ export const useEmailAutomationTableStructure: UseTableStructureHook<
18
+ EmailAutomationInterface,
19
+ EmailAutomationFields
20
+ > = (params) => {
21
+ const t = useTranslations();
22
+ const generateUrl = usePageUrlGenerator();
23
+ const triggerLabels: Record<string, string> = params.context?.triggerLabels ?? {};
24
+
25
+ const tableData = useMemo(() => {
26
+ return params.data.map((automation: EmailAutomationInterface) => {
27
+ const entry: TableContent<EmailAutomationInterface> = { jsonApiData: automation };
28
+ params.fields.forEach((field) => {
29
+ entry[field] = (automation as any)[field as keyof EmailAutomationInterface];
30
+ });
31
+ return entry;
32
+ });
33
+ }, [params.data, params.fields]);
34
+
35
+ // Typography role 15 (numeric): tabular-nums, end-aligned. NEVER
36
+ // font-mono — no monospace font is loaded in these apps.
37
+ const numericColumn = (
38
+ field: EmailAutomationFields,
39
+ header: string,
40
+ read: (a: EmailAutomationInterface) => number,
41
+ ) => ({
42
+ id: field,
43
+ accessorKey: field,
44
+ header: header,
45
+ cell: ({ row }: { row: TableContent<EmailAutomationInterface> }) => {
46
+ const automation: EmailAutomationInterface = row.original.jsonApiData;
47
+ return <span className="block text-xs tabular-nums text-end">{read(automation) ?? 0}</span>;
48
+ },
49
+ enableSorting: false,
50
+ enableHiding: false,
51
+ });
52
+
53
+ const fieldColumnMap: Partial<Record<EmailAutomationFields, () => any>> = {
54
+ [EmailAutomationFields.name]: () => ({
55
+ id: "name",
56
+ accessorKey: "name",
57
+ header: t("emailAutomations.list.columns.name"),
58
+ cell: ({ row }: { row: TableContent<EmailAutomationInterface> }) => {
59
+ const automation: EmailAutomationInterface = row.original.jsonApiData;
60
+ return <Link href={generateUrl({ page: Modules.EmailAutomation, id: automation.id })}>{automation.name}</Link>;
61
+ },
62
+ enableSorting: false,
63
+ enableHiding: false,
64
+ }),
65
+ [EmailAutomationFields.trigger]: () => ({
66
+ id: "trigger",
67
+ accessorKey: "trigger",
68
+ header: t("emailAutomations.list.columns.trigger"),
69
+ cell: ({ row }: { row: TableContent<EmailAutomationInterface> }) => {
70
+ const automation: EmailAutomationInterface = row.original.jsonApiData;
71
+ if (!automation.trigger) return <span className="text-muted-foreground text-xs">—</span>;
72
+ return <span className="text-sm">{triggerLabels[automation.trigger] ?? automation.trigger}</span>;
73
+ },
74
+ enableSorting: false,
75
+ enableHiding: false,
76
+ }),
77
+ [EmailAutomationFields.status]: () => ({
78
+ id: "status",
79
+ accessorKey: "status",
80
+ header: t("emailAutomations.list.columns.status"),
81
+ cell: ({ row }: { row: TableContent<EmailAutomationInterface> }) => {
82
+ const automation: EmailAutomationInterface = row.original.jsonApiData;
83
+ return automation.isActive ? (
84
+ <Badge variant="softGreen">{t("emailAutomations.list.status.on")}</Badge>
85
+ ) : (
86
+ <Badge variant="softGray">{t("emailAutomations.list.status.off")}</Badge>
87
+ );
88
+ },
89
+ enableSorting: false,
90
+ enableHiding: false,
91
+ }),
92
+ [EmailAutomationFields.steps]: () =>
93
+ numericColumn(EmailAutomationFields.steps, t("emailAutomations.list.columns.steps"), (a) => a.stepCount),
94
+ [EmailAutomationFields.active]: () =>
95
+ numericColumn(EmailAutomationFields.active, t("emailAutomations.list.columns.active"), (a) => a.activeCount),
96
+ [EmailAutomationFields.completed]: () =>
97
+ numericColumn(
98
+ EmailAutomationFields.completed,
99
+ t("emailAutomations.list.columns.completed"),
100
+ (a) => a.completedCount,
101
+ ),
102
+ [EmailAutomationFields.unsubscribed]: () =>
103
+ numericColumn(
104
+ EmailAutomationFields.unsubscribed,
105
+ t("emailAutomations.list.columns.unsubscribed"),
106
+ (a) => a.unsubscribedCount,
107
+ ),
108
+ };
109
+
110
+ const columns = useMemo(() => {
111
+ return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
112
+ TableContent<EmailAutomationInterface>
113
+ >[];
114
+ }, [params.fields, fieldColumnMap, t, generateUrl]);
115
+
116
+ return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
117
+ };
118
+
119
+ registerTableGenerator("email-automations", useEmailAutomationTableStructure);
@@ -0,0 +1,5 @@
1
+ export * from "./EmailAutomationModule";
2
+ export * from "./EmailAutomationStepModule";
3
+ export * from "./EmailAutomationEnrollmentModule";
4
+ export * from "./EmailAutomationTriggerModule";
5
+ export * from "./data";