@carlonicora/nextjs-jsonapi 5.2.0 → 5.4.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 (237) hide show
  1. package/dist/{BlockNoteEditor-EGY4ZAAM.js → BlockNoteEditor-4YCAIEPT.js} +23 -22
  2. package/dist/BlockNoteEditor-4YCAIEPT.js.map +1 -0
  3. package/dist/{BlockNoteEditor-2QJVK2HH.mjs → BlockNoteEditor-YQL2SGFR.mjs} +8 -7
  4. package/dist/BlockNoteEditor-YQL2SGFR.mjs.map +1 -0
  5. package/dist/{BlockNoteViewer-6BQXJ4TB.mjs → BlockNoteViewer-AEJKJ27Q.mjs} +2 -2
  6. package/dist/{BlockNoteViewer-I2MMFH3S.js → BlockNoteViewer-YL3J7J2Y.js} +3 -3
  7. package/dist/{BlockNoteViewer-I2MMFH3S.js.map → BlockNoteViewer-YL3J7J2Y.js.map} +1 -1
  8. package/dist/BlogCategoryFields-DO48vOTE.d.mts +14 -0
  9. package/dist/BlogCategoryFields-DO48vOTE.d.ts +14 -0
  10. package/dist/BlogPostInterface-BB_4wcDR.d.mts +124 -0
  11. package/dist/BlogPostInterface-bWAT1rfR.d.ts +124 -0
  12. package/dist/billing/index.js +441 -441
  13. package/dist/billing/index.mjs +3 -3
  14. package/dist/blog-public.types-BbrzsC1c.d.mts +30 -0
  15. package/dist/blog-public.types-BbrzsC1c.d.ts +30 -0
  16. package/dist/{chunk-3VEQ5JZJ.js → chunk-3NLAXVIN.js} +860 -849
  17. package/dist/{chunk-3VEQ5JZJ.js.map → chunk-3NLAXVIN.js.map} +1 -1
  18. package/dist/{chunk-YMUPTXOT.mjs → chunk-3YW35YHQ.mjs} +1 -101
  19. package/dist/chunk-3YW35YHQ.mjs.map +1 -0
  20. package/dist/{chunk-EXICQC4A.js → chunk-5PPG6PL7.js} +3 -3
  21. package/dist/{chunk-EXICQC4A.js.map → chunk-5PPG6PL7.js.map} +1 -1
  22. package/dist/{chunk-NPKFMRH5.mjs → chunk-6DC2WVVB.mjs} +2 -2
  23. package/dist/{chunk-2Z3ZCYXX.mjs → chunk-EUKTBOI2.mjs} +19 -8
  24. package/dist/chunk-EUKTBOI2.mjs.map +1 -0
  25. package/dist/{chunk-THKYH65N.js → chunk-KSYQEKJX.js} +11 -111
  26. package/dist/chunk-KSYQEKJX.js.map +1 -0
  27. package/dist/{chunk-S5MQDWWC.js → chunk-NFBOSPNJ.js} +1451 -176
  28. package/dist/chunk-NFBOSPNJ.js.map +1 -0
  29. package/dist/{chunk-JEZQTZQ4.mjs → chunk-QMYQRPQC.mjs} +2 -2
  30. package/dist/{chunk-HNOWSFM5.mjs → chunk-WC63PJSC.mjs} +1278 -3
  31. package/dist/chunk-WC63PJSC.mjs.map +1 -0
  32. package/dist/chunk-XCHSX3VC.mjs +107 -0
  33. package/dist/chunk-XCHSX3VC.mjs.map +1 -0
  34. package/dist/{chunk-75MLT74B.js → chunk-YNCQSWQW.js} +8 -8
  35. package/dist/{chunk-75MLT74B.js.map → chunk-YNCQSWQW.js.map} +1 -1
  36. package/dist/chunk-YW4ZCEWB.js +107 -0
  37. package/dist/chunk-YW4ZCEWB.js.map +1 -0
  38. package/dist/client/index.d.mts +6 -39
  39. package/dist/client/index.d.ts +6 -39
  40. package/dist/client/index.js +4 -4
  41. package/dist/client/index.mjs +3 -3
  42. package/dist/components/index.d.mts +15 -4
  43. package/dist/components/index.d.ts +15 -4
  44. package/dist/components/index.js +4 -4
  45. package/dist/components/index.mjs +3 -3
  46. package/dist/contexts/index.js +4 -4
  47. package/dist/contexts/index.mjs +3 -3
  48. package/dist/core/index.d.mts +400 -3
  49. package/dist/core/index.d.ts +400 -3
  50. package/dist/core/index.js +64 -2
  51. package/dist/core/index.js.map +1 -1
  52. package/dist/core/index.mjs +63 -1
  53. package/dist/email-automation-enrollment.fields-BZvF14qx.d.mts +82 -0
  54. package/dist/email-automation-enrollment.fields-Cp3VMzFq.d.ts +82 -0
  55. package/dist/email-automations/index.css +30 -0
  56. package/dist/email-automations/index.css.map +1 -0
  57. package/dist/email-automations/index.d.mts +303 -0
  58. package/dist/email-automations/index.d.ts +303 -0
  59. package/dist/email-automations/index.js +1705 -0
  60. package/dist/email-automations/index.js.map +1 -0
  61. package/dist/email-automations/index.mjs +1705 -0
  62. package/dist/email-automations/index.mjs.map +1 -0
  63. package/dist/features/analytics/index.js +122 -122
  64. package/dist/features/analytics/index.mjs +4 -4
  65. package/dist/features/analytics/server.js +6 -5
  66. package/dist/features/analytics/server.js.map +1 -1
  67. package/dist/features/analytics/server.mjs +4 -3
  68. package/dist/features/analytics/server.mjs.map +1 -1
  69. package/dist/features/blog/index.css +30 -0
  70. package/dist/features/blog/index.css.map +1 -0
  71. package/dist/features/blog/index.d.mts +266 -0
  72. package/dist/features/blog/index.d.ts +266 -0
  73. package/dist/features/blog/index.js +1799 -0
  74. package/dist/features/blog/index.js.map +1 -0
  75. package/dist/features/blog/index.mjs +1799 -0
  76. package/dist/features/blog/index.mjs.map +1 -0
  77. package/dist/features/blog/server.d.mts +33 -0
  78. package/dist/features/blog/server.d.ts +33 -0
  79. package/dist/features/blog/server.js +139 -0
  80. package/dist/features/blog/server.js.map +1 -0
  81. package/dist/features/blog/server.mjs +139 -0
  82. package/dist/features/blog/server.mjs.map +1 -0
  83. package/dist/features/help/index.js +37 -37
  84. package/dist/features/help/index.mjs +3 -3
  85. package/dist/features/tokenusage/index.js +125 -125
  86. package/dist/features/tokenusage/index.mjs +4 -4
  87. package/dist/index.d.mts +5 -2
  88. package/dist/index.d.ts +5 -2
  89. package/dist/index.js +65 -3
  90. package/dist/index.js.map +1 -1
  91. package/dist/index.mjs +64 -2
  92. package/dist/{s3.service-CY-jgOAM.d.ts → s3.service-Bz_K-tjz.d.ts} +11 -0
  93. package/dist/{s3.service-Sri0QP7Z.d.mts → s3.service-D5zNCEW3.d.mts} +11 -0
  94. package/dist/server/index.d.mts +1 -1
  95. package/dist/server/index.d.ts +1 -1
  96. package/dist/server/index.js +8 -6
  97. package/dist/server/index.js.map +1 -1
  98. package/dist/server/index.mjs +12 -10
  99. package/dist/types-70acd-8C.d.mts +39 -0
  100. package/dist/types-l8SFleXi.d.ts +39 -0
  101. package/messages/en.json +210 -0
  102. package/messages/it.json +210 -0
  103. package/package.json +21 -1
  104. package/src/components/editors/BlockNoteEditor.tsx +13 -3
  105. package/src/components/forms/FormBlockNote.tsx +4 -0
  106. package/src/components/forms/FormFieldWrapper.tsx +1 -1
  107. package/src/components/forms/FormTextarea.tsx +2 -1
  108. package/src/components/navigations/ContentTitle.tsx +14 -3
  109. package/src/core/index.ts +12 -0
  110. package/src/core/registry/ModuleRegistry.ts +11 -0
  111. package/src/email-automations/index.ts +5 -0
  112. package/src/features/blog/BlogModules.ts +49 -0
  113. package/src/features/blog/components/admin/BlogAdminContainer.tsx +32 -0
  114. package/src/features/blog/components/admin/BlogCategoriesList.tsx +56 -0
  115. package/src/features/blog/components/admin/BlogCategoryEditor.tsx +230 -0
  116. package/src/features/blog/components/admin/BlogCoverImageField.tsx +123 -0
  117. package/src/features/blog/components/admin/BlogLayoutSetting.tsx +86 -0
  118. package/src/features/blog/components/admin/BlogPostComparison.tsx +54 -0
  119. package/src/features/blog/components/admin/BlogPostContainer.tsx +102 -0
  120. package/src/features/blog/components/admin/BlogPostDeleter.tsx +35 -0
  121. package/src/features/blog/components/admin/BlogPostEditor.tsx +207 -0
  122. package/src/features/blog/components/admin/BlogPostPublishButton.tsx +50 -0
  123. package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +110 -0
  124. package/src/features/blog/components/admin/BlogPostTranslationPanel.tsx +136 -0
  125. package/src/features/blog/components/admin/BlogPostsList.tsx +58 -0
  126. package/src/features/blog/components/admin/BlogTranslationStatus.tsx +104 -0
  127. package/src/features/blog/components/admin/__tests__/BlogPostTranslationPanel.spec.tsx +100 -0
  128. package/src/features/blog/components/admin/__tests__/BlogPostsList.spec.tsx +30 -0
  129. package/src/features/blog/components/admin/index.ts +14 -0
  130. package/src/features/blog/components/public/BlogCategoryChips.tsx +59 -0
  131. package/src/features/blog/components/public/BlogList.tsx +21 -0
  132. package/src/features/blog/components/public/BlogListColumn.tsx +53 -0
  133. package/src/features/blog/components/public/BlogListFeatured.tsx +57 -0
  134. package/src/features/blog/components/public/BlogListParts.tsx +74 -0
  135. package/src/features/blog/components/public/BlogListRail.tsx +62 -0
  136. package/src/features/blog/components/public/BlogPostCard.tsx +132 -0
  137. package/src/features/blog/components/public/BlogPostMeta.tsx +49 -0
  138. package/src/features/blog/components/public/BlogPostView.tsx +74 -0
  139. package/src/features/blog/components/public/BlogSearchBox.tsx +52 -0
  140. package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +173 -0
  141. package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +128 -0
  142. package/src/features/blog/components/public/blogPublicHelpers.ts +30 -0
  143. package/src/features/blog/components/public/index.ts +3 -0
  144. package/src/features/blog/contexts/BlogPostContext.tsx +66 -0
  145. package/src/features/blog/contexts/index.ts +1 -0
  146. package/src/features/blog/data/BlogCategory.ts +93 -0
  147. package/src/features/blog/data/BlogCategoryFields.ts +4 -0
  148. package/src/features/blog/data/BlogCategoryInterface.ts +24 -0
  149. package/src/features/blog/data/BlogCategoryService.ts +54 -0
  150. package/src/features/blog/data/BlogCategoryTranslation.ts +90 -0
  151. package/src/features/blog/data/BlogCategoryTranslationInterface.ts +22 -0
  152. package/src/features/blog/data/BlogCategoryTranslationService.ts +30 -0
  153. package/src/features/blog/data/BlogPost.spec.ts +123 -0
  154. package/src/features/blog/data/BlogPost.ts +159 -0
  155. package/src/features/blog/data/BlogPostFields.ts +7 -0
  156. package/src/features/blog/data/BlogPostInterface.ts +39 -0
  157. package/src/features/blog/data/BlogPostService.ts +82 -0
  158. package/src/features/blog/data/BlogPostTranslation.spec.ts +73 -0
  159. package/src/features/blog/data/BlogPostTranslation.ts +110 -0
  160. package/src/features/blog/data/BlogPostTranslationInterface.ts +27 -0
  161. package/src/features/blog/data/BlogPostTranslationService.ts +30 -0
  162. package/src/features/blog/data/BlogSettings.ts +37 -0
  163. package/src/features/blog/data/BlogSettingsInterface.ts +11 -0
  164. package/src/features/blog/data/BlogSettingsService.ts +24 -0
  165. package/src/features/blog/data/index.ts +17 -0
  166. package/src/features/blog/hooks/index.ts +3 -0
  167. package/src/features/blog/hooks/useBlogCategoryNames.ts +34 -0
  168. package/src/features/blog/hooks/useBlogCategoryTableStructure.tsx +84 -0
  169. package/src/features/blog/hooks/useBlogPostTableStructure.tsx +126 -0
  170. package/src/features/blog/index.ts +15 -0
  171. package/src/features/blog/server/fetchers.ts +120 -0
  172. package/src/features/blog/server/toPublic.spec.ts +91 -0
  173. package/src/features/blog/server/toPublic.ts +36 -0
  174. package/src/features/blog/server-entry.ts +17 -0
  175. package/src/features/blog/types/blog-public.types.ts +34 -0
  176. package/src/features/blog/types/blog.constants.ts +40 -0
  177. package/src/features/email-automation/EmailAutomationEnrollmentModule.ts +11 -0
  178. package/src/features/email-automation/EmailAutomationModule.ts +30 -0
  179. package/src/features/email-automation/EmailAutomationStepModule.ts +11 -0
  180. package/src/features/email-automation/EmailAutomationTriggerModule.ts +11 -0
  181. package/src/features/email-automation/components/__tests__/EmailAutomationEditor.spec.tsx +104 -0
  182. package/src/features/email-automation/components/canvas/EmailAutomationActiveSwitch.tsx +49 -0
  183. package/src/features/email-automation/components/canvas/EmailAutomationCanvas.tsx +474 -0
  184. package/src/features/email-automation/components/canvas/EmailAutomationCanvasInspector.tsx +89 -0
  185. package/src/features/email-automation/components/canvas/EmailAutomationCanvasTab.tsx +32 -0
  186. package/src/features/email-automation/components/canvas/__tests__/buildCanvasGraph.spec.ts +67 -0
  187. package/src/features/email-automation/components/canvas/buildCanvasGraph.ts +141 -0
  188. package/src/features/email-automation/components/canvas/edges/DelayEdge.tsx +68 -0
  189. package/src/features/email-automation/components/canvas/nodes/EmailStepNode.tsx +43 -0
  190. package/src/features/email-automation/components/canvas/nodes/TriggerNode.tsx +23 -0
  191. package/src/features/email-automation/components/canvas/shared/CanvasToolbar.tsx +53 -0
  192. package/src/features/email-automation/components/canvas/shared/EmptyCanvas.tsx +11 -0
  193. package/src/features/email-automation/components/canvas/shared/__tests__/autoLayout.spec.ts +42 -0
  194. package/src/features/email-automation/components/canvas/shared/__tests__/longPress.spec.ts +51 -0
  195. package/src/features/email-automation/components/canvas/shared/autoLayout.ts +55 -0
  196. package/src/features/email-automation/components/canvas/shared/index.ts +4 -0
  197. package/src/features/email-automation/components/canvas/shared/longPress.ts +57 -0
  198. package/src/features/email-automation/components/containers/EmailAutomationContainer.tsx +80 -0
  199. package/src/features/email-automation/components/containers/EmailAutomationsListContainer.tsx +27 -0
  200. package/src/features/email-automation/components/forms/EmailAutomationEditor.tsx +153 -0
  201. package/src/features/email-automation/components/forms/EmailStepEditor.tsx +151 -0
  202. package/src/features/email-automation/components/index.ts +15 -0
  203. package/src/features/email-automation/components/lists/EmailAutomationRecipientsList.tsx +90 -0
  204. package/src/features/email-automation/components/lists/EmailAutomationsList.tsx +76 -0
  205. package/src/features/email-automation/components/unsubscribe/EmailUnsubscribe.tsx +99 -0
  206. package/src/features/email-automation/components/unsubscribe/__tests__/EmailUnsubscribe.spec.tsx +55 -0
  207. package/src/features/email-automation/contexts/EmailAutomationContext.tsx +175 -0
  208. package/src/features/email-automation/data/EmailAutomation.ts +87 -0
  209. package/src/features/email-automation/data/EmailAutomationEnrollment.ts +105 -0
  210. package/src/features/email-automation/data/EmailAutomationEnrollmentInterface.ts +18 -0
  211. package/src/features/email-automation/data/EmailAutomationEnrollmentService.ts +31 -0
  212. package/src/features/email-automation/data/EmailAutomationInterface.ts +22 -0
  213. package/src/features/email-automation/data/EmailAutomationService.ts +106 -0
  214. package/src/features/email-automation/data/EmailAutomationStep.ts +104 -0
  215. package/src/features/email-automation/data/EmailAutomationStepInterface.ts +23 -0
  216. package/src/features/email-automation/data/EmailAutomationStepService.ts +80 -0
  217. package/src/features/email-automation/data/EmailAutomationTrigger.ts +28 -0
  218. package/src/features/email-automation/data/EmailAutomationTriggerInterface.ts +6 -0
  219. package/src/features/email-automation/data/__tests__/email-automation-models.spec.ts +103 -0
  220. package/src/features/email-automation/data/email-automation-enrollment.fields.ts +8 -0
  221. package/src/features/email-automation/data/email-automation.fields.ts +9 -0
  222. package/src/features/email-automation/data/index.ts +13 -0
  223. package/src/features/email-automation/hooks/index.ts +2 -0
  224. package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +126 -0
  225. package/src/features/email-automation/hooks/useEmailAutomationTableStructure.tsx +119 -0
  226. package/src/features/email-automation/index.ts +5 -0
  227. package/src/features/s3/data/s3.service.ts +17 -0
  228. package/dist/BlockNoteEditor-2QJVK2HH.mjs.map +0 -1
  229. package/dist/BlockNoteEditor-EGY4ZAAM.js.map +0 -1
  230. package/dist/chunk-2Z3ZCYXX.mjs.map +0 -1
  231. package/dist/chunk-HNOWSFM5.mjs.map +0 -1
  232. package/dist/chunk-S5MQDWWC.js.map +0 -1
  233. package/dist/chunk-THKYH65N.js.map +0 -1
  234. package/dist/chunk-YMUPTXOT.mjs.map +0 -1
  235. /package/dist/{BlockNoteViewer-6BQXJ4TB.mjs.map → BlockNoteViewer-AEJKJ27Q.mjs.map} +0 -0
  236. /package/dist/{chunk-NPKFMRH5.mjs.map → chunk-6DC2WVVB.mjs.map} +0 -0
  237. /package/dist/{chunk-JEZQTZQ4.mjs.map → chunk-QMYQRPQC.mjs.map} +0 -0
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { RoundPageContainer } from "../../../../components";
5
+ import { SharedProvider } from "../../../../contexts/SharedContext";
6
+ import { Modules } from "../../../../core";
7
+ import { usePageUrlGenerator } from "../../../../hooks";
8
+ import { BreadcrumbItemData } from "../../../../interfaces";
9
+ import { EmailAutomationProvider, useEmailAutomation } from "../../contexts/EmailAutomationContext";
10
+ import { EmailAutomationActiveSwitch } from "../canvas/EmailAutomationActiveSwitch";
11
+ import { EmailAutomationCanvasTab } from "../canvas/EmailAutomationCanvasTab";
12
+ import { EmailAutomationEditor } from "../forms/EmailAutomationEditor";
13
+ import { EmailAutomationRecipientsList } from "../lists/EmailAutomationRecipientsList";
14
+
15
+ export type EmailAutomationContainerProps = {
16
+ automationId: string;
17
+ };
18
+
19
+ export function EmailAutomationContainer({ automationId }: EmailAutomationContainerProps) {
20
+ return (
21
+ <EmailAutomationProvider automationId={automationId}>
22
+ <EmailAutomationShell />
23
+ </EmailAutomationProvider>
24
+ );
25
+ }
26
+
27
+ function EmailAutomationShell() {
28
+ const t = useTranslations();
29
+ const generateUrl = usePageUrlGenerator();
30
+ const { automation, reload } = useEmailAutomation();
31
+
32
+ if (!automation) return null;
33
+
34
+ const breadcrumbs: BreadcrumbItemData[] = [
35
+ { name: t("emailAutomations.admin.title"), href: generateUrl({ page: Modules.EmailAutomation }) },
36
+ { name: automation.name },
37
+ ];
38
+
39
+ // The page chrome (title + header functions) is published here rather than by
40
+ // AdministrationProvider: SharedProvider replaces its value for the subtree,
41
+ // so this one carries the administration title type plus the automation name.
42
+ return (
43
+ <SharedProvider
44
+ value={{
45
+ breadcrumbs,
46
+ title: {
47
+ type: t("emailAutomations.admin.title"),
48
+ element: automation.name,
49
+ functions: [
50
+ <EmailAutomationActiveSwitch key="emailAutomationActiveSwitch" />,
51
+ <EmailAutomationEditor key="emailAutomationEditor" automation={automation} onSuccess={reload} />,
52
+ ],
53
+ },
54
+ }}
55
+ >
56
+ <RoundPageContainer
57
+ module={Modules.EmailAutomation}
58
+ id={automation.id}
59
+ fullWidth
60
+ forceHeader
61
+ // The canvas is sized h-full: it needs the viewport-bound shell and a
62
+ // fillHeight tab, or it resolves to zero height.
63
+ scroll="fixed"
64
+ tabs={[
65
+ {
66
+ sectionKey: "canvas",
67
+ label: t("emailAutomations.tabs.canvas"),
68
+ content: <EmailAutomationCanvasTab />,
69
+ fillHeight: true,
70
+ },
71
+ {
72
+ sectionKey: "recipients",
73
+ label: t("emailAutomations.tabs.recipients"),
74
+ content: <EmailAutomationRecipientsList />,
75
+ },
76
+ ]}
77
+ />
78
+ </SharedProvider>
79
+ );
80
+ }
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { RoundPageContainer } from "../../../../components";
4
+ import { Modules } from "../../../../core";
5
+ import { AdministrationProvider } from "../../../administration/contexts/AdministrationContext";
6
+ import { EmailAutomationsList } from "../lists/EmailAutomationsList";
7
+
8
+ /**
9
+ * Page container for the administrative email-automation list.
10
+ *
11
+ * Client component by necessity: `module={Modules.EmailAutomation}` is a
12
+ * registry entry carrying an icon component and methods, which a Server
13
+ * Component cannot pass across the boundary. Routes mount this container
14
+ * instead — the same shape as every other list page.
15
+ *
16
+ * No `forceHeader`: ContentListTable draws its own header row with the title
17
+ * and the create action, exactly as the company and user list pages do.
18
+ */
19
+ export function EmailAutomationsListContainer() {
20
+ return (
21
+ <AdministrationProvider>
22
+ <RoundPageContainer module={Modules.EmailAutomation} fullWidth>
23
+ <EmailAutomationsList fullWidth />
24
+ </RoundPageContainer>
25
+ </AdministrationProvider>
26
+ );
27
+ }
@@ -0,0 +1,153 @@
1
+ "use client";
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { useTranslations } from "next-intl";
5
+ import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
6
+ import { useForm } from "react-hook-form";
7
+ import { v4 } from "uuid";
8
+ import { z } from "zod";
9
+ import { EditorSheet, FormInput, FormSelect, FormSwitch } from "../../../../components";
10
+ import { Modules } from "../../../../core";
11
+ import { useI18nRouter } from "../../../../i18n";
12
+ import { EmailAutomationInput, EmailAutomationInterface } from "../../data/EmailAutomationInterface";
13
+ import { EmailAutomationService } from "../../data/EmailAutomationService";
14
+ import { EmailAutomationTriggerInterface } from "../../data/EmailAutomationTriggerInterface";
15
+
16
+ export type EmailAutomationEditorProps = {
17
+ automation?: EmailAutomationInterface;
18
+ propagateChanges?: (automation: EmailAutomationInterface) => void;
19
+ onSuccess?: () => void | Promise<void>;
20
+ trigger?: ReactNode;
21
+ forceShow?: boolean;
22
+ onClose?: () => void;
23
+ dialogOpen?: boolean;
24
+ onDialogOpenChange?: (open: boolean) => void;
25
+ };
26
+
27
+ function EmailAutomationEditorInternal({
28
+ automation,
29
+ propagateChanges,
30
+ onSuccess,
31
+ trigger,
32
+ forceShow,
33
+ onClose,
34
+ dialogOpen,
35
+ onDialogOpenChange,
36
+ }: EmailAutomationEditorProps) {
37
+ const t = useTranslations();
38
+ const router = useI18nRouter();
39
+ const isEdit = !!automation;
40
+ const [triggers, setTriggers] = useState<EmailAutomationTriggerInterface[]>([]);
41
+
42
+ // Trigger labels are plain strings from the app config, not i18n keys.
43
+ useEffect(() => {
44
+ let active = true;
45
+ EmailAutomationService.findTriggers()
46
+ .then((list) => {
47
+ if (active) setTriggers(list);
48
+ })
49
+ .catch(() => {});
50
+ return () => {
51
+ active = false;
52
+ };
53
+ }, []);
54
+
55
+ const triggerValues = useMemo(() => triggers.map((entry) => ({ id: entry.key, text: entry.label })), [triggers]);
56
+
57
+ const formSchema = useMemo(
58
+ () =>
59
+ z.object({
60
+ id: z.uuidv4(),
61
+ name: z.string().min(1, { message: t("emailAutomations.fields.name.error") }),
62
+ trigger: z.string().optional(),
63
+ exitTrigger: z.string().optional(),
64
+ replyTo: z.string().refine((value) => value === "" || z.email().safeParse(value).success, {
65
+ message: t("emailAutomations.fields.replyTo.error"),
66
+ }),
67
+ isActive: z.boolean(),
68
+ }),
69
+ [t],
70
+ );
71
+
72
+ const getDefaultValues = useCallback(
73
+ () => ({
74
+ id: automation?.id ?? v4(),
75
+ name: automation?.name ?? "",
76
+ trigger: automation?.trigger ?? "",
77
+ exitTrigger: automation?.exitTrigger ?? "",
78
+ replyTo: automation?.replyTo ?? "",
79
+ isActive: automation?.isActive ?? false,
80
+ }),
81
+ [automation],
82
+ );
83
+
84
+ const form = useForm<z.infer<typeof formSchema>>({
85
+ resolver: zodResolver(formSchema),
86
+ defaultValues: getDefaultValues(),
87
+ });
88
+
89
+ return (
90
+ <EditorSheet
91
+ form={form}
92
+ entityType={t("emailAutomations.admin.title")}
93
+ entityName={automation?.name}
94
+ title={isEdit ? t("emailAutomations.admin.editAutomation") : t("emailAutomations.admin.newAutomation")}
95
+ isEdit={isEdit}
96
+ module={Modules.EmailAutomation}
97
+ size="md"
98
+ propagateChanges={propagateChanges}
99
+ onSuccess={onSuccess}
100
+ onNavigate={(url) => router.push(url)}
101
+ onSubmit={async (values) => {
102
+ const payload: EmailAutomationInput = {
103
+ id: values.id,
104
+ name: values.name,
105
+ trigger: values.trigger || undefined,
106
+ exitTrigger: values.exitTrigger || undefined,
107
+ replyTo: values.replyTo || undefined,
108
+ isActive: values.isActive,
109
+ };
110
+ return isEdit ? await EmailAutomationService.update(payload) : await EmailAutomationService.create(payload);
111
+ }}
112
+ onReset={getDefaultValues}
113
+ trigger={trigger}
114
+ forceShow={forceShow}
115
+ onClose={onClose}
116
+ dialogOpen={dialogOpen}
117
+ onDialogOpenChange={onDialogOpenChange}
118
+ >
119
+ <div className="flex w-full flex-col gap-y-4">
120
+ <FormInput form={form} id="name" name={t("emailAutomations.fields.name.label")} isRequired />
121
+ <FormSelect
122
+ form={form}
123
+ id="trigger"
124
+ name={t("emailAutomations.fields.trigger.label")}
125
+ placeholder={t("emailAutomations.fields.trigger.none")}
126
+ values={triggerValues}
127
+ allowEmpty
128
+ />
129
+ <FormSelect
130
+ form={form}
131
+ id="exitTrigger"
132
+ name={t("emailAutomations.fields.exitTrigger.label")}
133
+ placeholder={t("emailAutomations.fields.trigger.none")}
134
+ values={triggerValues}
135
+ allowEmpty
136
+ />
137
+ <FormInput form={form} id="replyTo" name={t("emailAutomations.fields.replyTo.label")} />
138
+ <FormSwitch
139
+ form={form}
140
+ id="isActive"
141
+ name={t("emailAutomations.fields.isActive.label")}
142
+ description={t("emailAutomations.fields.isActive.help")}
143
+ />
144
+ </div>
145
+ </EditorSheet>
146
+ );
147
+ }
148
+
149
+ export function EmailAutomationEditor(props: EmailAutomationEditorProps) {
150
+ return <EmailAutomationEditorInternal {...props} />;
151
+ }
152
+
153
+ export default EmailAutomationEditor;
@@ -0,0 +1,151 @@
1
+ "use client";
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { useTranslations } from "next-intl";
5
+ import { ReactNode, useCallback, useMemo } from "react";
6
+ import { useForm } from "react-hook-form";
7
+ import { v4 } from "uuid";
8
+ import { z } from "zod";
9
+ import { EditorSheet, FormInput, FormTextarea } from "../../../../components";
10
+ import { Modules } from "../../../../core";
11
+ import { useEmailAutomation } from "../../contexts/EmailAutomationContext";
12
+ import { EmailAutomationStepInput, EmailAutomationStepInterface } from "../../data/EmailAutomationStepInterface";
13
+ import { EmailAutomationStepService } from "../../data/EmailAutomationStepService";
14
+
15
+ /** The fixed variable set the renderer fills (spec § "Rendering"). */
16
+ const TEMPLATE_VARIABLES = ["{{name}}", "{{email}}", "{{url}}", "{{unsubscribeUrl}}", "{{unsubscribeAllUrl}}"];
17
+
18
+ export type EmailStepEditorProps = {
19
+ step?: EmailAutomationStepInterface;
20
+ automationId: string;
21
+ /** Canvas position for a step created from the editor. */
22
+ initialPosition?: { x: number; y: number };
23
+ /** The head step's delay counts from entering the automation, not from a previous email. */
24
+ isHead?: boolean;
25
+ trigger?: ReactNode;
26
+ forceShow?: boolean;
27
+ onClose?: () => void;
28
+ dialogOpen?: boolean;
29
+ onDialogOpenChange?: (open: boolean) => void;
30
+ onSuccess?: () => void | Promise<void>;
31
+ };
32
+
33
+ function EmailStepEditorInternal({
34
+ step,
35
+ automationId,
36
+ initialPosition,
37
+ isHead,
38
+ trigger,
39
+ forceShow,
40
+ onClose,
41
+ dialogOpen,
42
+ onDialogOpenChange,
43
+ onSuccess,
44
+ }: EmailStepEditorProps) {
45
+ const t = useTranslations();
46
+ const { reload } = useEmailAutomation();
47
+ const isEdit = !!step;
48
+
49
+ const formSchema = useMemo(
50
+ () =>
51
+ z.object({
52
+ id: z.uuidv4(),
53
+ subject: z.string().min(1, { message: t("emailAutomations.step.fields.subject.error") }),
54
+ delayHours: z.number().int().min(0),
55
+ body: z.string().min(1, { message: t("emailAutomations.step.fields.body.error") }),
56
+ }),
57
+ [t],
58
+ );
59
+
60
+ const getDefaultValues = useCallback(
61
+ () => ({
62
+ id: step?.id ?? v4(),
63
+ subject: step?.subject ?? "",
64
+ delayHours: step?.delayHours ?? 24,
65
+ body: step?.body ?? "",
66
+ }),
67
+ [step],
68
+ );
69
+
70
+ const form = useForm<z.infer<typeof formSchema>>({
71
+ resolver: zodResolver(formSchema),
72
+ defaultValues: getDefaultValues(),
73
+ });
74
+
75
+ return (
76
+ <EditorSheet
77
+ form={form}
78
+ entityType={t("emailAutomations.step.title")}
79
+ entityName={step?.subject}
80
+ title={isEdit ? t("emailAutomations.step.editTitle") : t("emailAutomations.step.title")}
81
+ isEdit={isEdit}
82
+ module={Modules.EmailAutomationStep}
83
+ size="lg"
84
+ onSubmit={async (values) => {
85
+ const payload: EmailAutomationStepInput = {
86
+ id: values.id,
87
+ automationId,
88
+ subject: values.subject,
89
+ body: values.body,
90
+ delayHours: values.delayHours,
91
+ positionX: step ? step.positionX : initialPosition?.x,
92
+ positionY: step ? step.positionY : initialPosition?.y,
93
+ };
94
+ return isEdit
95
+ ? await EmailAutomationStepService.update(payload)
96
+ : await EmailAutomationStepService.create(payload);
97
+ }}
98
+ // Save → reload the canvas, then whatever the caller adds.
99
+ onSuccess={async () => {
100
+ await reload();
101
+ await onSuccess?.();
102
+ }}
103
+ onReset={getDefaultValues}
104
+ trigger={trigger}
105
+ forceShow={forceShow}
106
+ onClose={onClose}
107
+ dialogOpen={dialogOpen}
108
+ onDialogOpenChange={onDialogOpenChange}
109
+ >
110
+ <div className="flex w-full flex-col gap-y-4">
111
+ <FormInput form={form} id="subject" name={t("emailAutomations.step.fields.subject.label")} isRequired />
112
+ <FormInput
113
+ form={form}
114
+ id="delayHours"
115
+ type="number"
116
+ name={t("emailAutomations.step.fields.delayHours.label")}
117
+ description={
118
+ isHead ? t("emailAutomations.canvas.hoursAfterEntering") : t("emailAutomations.canvas.hoursAfterPrevious")
119
+ }
120
+ />
121
+ {/* FormTextarea has no `rows` prop; min-h-72 is the height of ~14 rows. */}
122
+ <FormTextarea
123
+ form={form}
124
+ id="body"
125
+ name={t("emailAutomations.step.fields.body.label")}
126
+ className="min-h-72"
127
+ isRequired
128
+ description={
129
+ <>
130
+ <span className="block">
131
+ {t("emailAutomations.step.variablesHint")}{" "}
132
+ {TEMPLATE_VARIABLES.map((variable) => (
133
+ <span key={variable} className="me-2 text-foreground">
134
+ {variable}
135
+ </span>
136
+ ))}
137
+ </span>
138
+ <span className="mt-1 block">{t("emailAutomations.step.unsubscribeHint")}</span>
139
+ </>
140
+ }
141
+ />
142
+ </div>
143
+ </EditorSheet>
144
+ );
145
+ }
146
+
147
+ export function EmailStepEditor(props: EmailStepEditorProps) {
148
+ return <EmailStepEditorInternal {...props} />;
149
+ }
150
+
151
+ export default EmailStepEditor;
@@ -0,0 +1,15 @@
1
+ export * from "./containers/EmailAutomationsListContainer";
2
+ export * from "./containers/EmailAutomationContainer";
3
+ export * from "./lists/EmailAutomationsList";
4
+ export * from "./lists/EmailAutomationRecipientsList";
5
+ export * from "./forms/EmailAutomationEditor";
6
+ export * from "./forms/EmailStepEditor";
7
+ export * from "./canvas/EmailAutomationActiveSwitch";
8
+ export * from "./canvas/EmailAutomationCanvasTab";
9
+ export * from "./canvas/EmailAutomationCanvas";
10
+ export * from "./canvas/EmailAutomationCanvasInspector";
11
+ export * from "./canvas/nodes/TriggerNode";
12
+ export * from "./canvas/nodes/EmailStepNode";
13
+ export * from "./canvas/edges/DelayEdge";
14
+ export * from "./canvas/buildCanvasGraph";
15
+ export * from "./unsubscribe/EmailUnsubscribe";
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ import { RefreshCw } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { useMemo, useState } from "react";
6
+ import { ContentListTable } from "../../../../components";
7
+ import { Modules } from "../../../../core";
8
+ import { DataListRetriever, useDataListRetriever } from "../../../../hooks";
9
+ import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../shadcnui";
10
+ import { useEmailAutomation } from "../../contexts/EmailAutomationContext";
11
+ import { EmailAutomationEnrollmentFields } from "../../data/email-automation-enrollment.fields";
12
+ import { EmailAutomationEnrollmentInterface } from "../../data/EmailAutomationEnrollmentInterface";
13
+ import { EmailAutomationService } from "../../data/EmailAutomationService";
14
+
15
+ /**
16
+ * Read-only list of the people in this automation (owner decision 6: no row
17
+ * actions). Rendered inside a tab, so it keeps its own bordered card (no
18
+ * `fullWidth`), as PricesList does inside ProductContainer.
19
+ */
20
+ export function EmailAutomationRecipientsList() {
21
+ const t = useTranslations();
22
+ const { automation } = useEmailAutomation();
23
+ const [statusFilter, setStatusFilter] = useState<string>("all");
24
+
25
+ const data: DataListRetriever<EmailAutomationEnrollmentInterface> = useDataListRetriever({
26
+ retriever: (params) => EmailAutomationService.findEnrollments(params),
27
+ retrieverParams: { automationId: automation?.id },
28
+ module: Modules.EmailAutomationEnrollment,
29
+ });
30
+
31
+ const handleStatusChange = (value: string | null) => {
32
+ const status = value ?? "all";
33
+ setStatusFilter(status);
34
+ data.addAdditionalParameter("status", status === "all" ? null : status);
35
+ };
36
+
37
+ const statusItems = useMemo(
38
+ () => ({
39
+ all: t("emailAutomations.recipients.status.all"),
40
+ active: t("emailAutomations.recipients.status.active"),
41
+ completed: t("emailAutomations.recipients.status.completed"),
42
+ unsubscribed: t("emailAutomations.recipients.status.unsubscribed"),
43
+ exited: t("emailAutomations.recipients.status.exited"),
44
+ failed: t("emailAutomations.recipients.status.failed"),
45
+ }),
46
+ [t],
47
+ );
48
+
49
+ const filters = (
50
+ <div className="flex items-center gap-4">
51
+ <Select items={statusItems} value={statusFilter} onValueChange={handleStatusChange}>
52
+ <SelectTrigger className="w-40">
53
+ <SelectValue />
54
+ </SelectTrigger>
55
+ <SelectContent>
56
+ <SelectItem value="all">{t("emailAutomations.recipients.status.all")}</SelectItem>
57
+ <SelectItem value="active">{t("emailAutomations.recipients.status.active")}</SelectItem>
58
+ <SelectItem value="completed">{t("emailAutomations.recipients.status.completed")}</SelectItem>
59
+ <SelectItem value="unsubscribed">{t("emailAutomations.recipients.status.unsubscribed")}</SelectItem>
60
+ <SelectItem value="exited">{t("emailAutomations.recipients.status.exited")}</SelectItem>
61
+ <SelectItem value="failed">{t("emailAutomations.recipients.status.failed")}</SelectItem>
62
+ </SelectContent>
63
+ </Select>
64
+
65
+ <Button variant="outline" size="icon" onClick={() => void data.refresh()} disabled={!data.isLoaded}>
66
+ <RefreshCw className={`h-4 w-4 ${!data.isLoaded ? "animate-spin" : ""}`} />
67
+ </Button>
68
+ </div>
69
+ );
70
+
71
+ return (
72
+ <ContentListTable
73
+ data={data}
74
+ fields={[
75
+ EmailAutomationEnrollmentFields.email,
76
+ EmailAutomationEnrollmentFields.name,
77
+ EmailAutomationEnrollmentFields.status,
78
+ EmailAutomationEnrollmentFields.currentStep,
79
+ EmailAutomationEnrollmentFields.nextSendAt,
80
+ EmailAutomationEnrollmentFields.sentCount,
81
+ ]}
82
+ tableGeneratorType={Modules.EmailAutomationEnrollment}
83
+ filters={filters}
84
+ /* The enrolments endpoint takes no search term, so rendering the box would
85
+ give the page a control that silently does nothing. */
86
+ allowSearch={false}
87
+ emptyState={t("emailAutomations.recipients.empty")}
88
+ />
89
+ );
90
+ }
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import { MailIcon } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { ReactNode, useEffect, useMemo, useState } from "react";
6
+ import { ContentListTable } from "../../../../components";
7
+ import { Modules } from "../../../../core";
8
+ import { DataListRetriever, useDataListRetriever } from "../../../../hooks";
9
+ import { EmptyState } from "../../../../shadcnui";
10
+ import { EmailAutomationFields } from "../../data/email-automation.fields";
11
+ import { EmailAutomationInterface } from "../../data/EmailAutomationInterface";
12
+ import { EmailAutomationService } from "../../data/EmailAutomationService";
13
+ import { EmailAutomationTriggerInterface } from "../../data/EmailAutomationTriggerInterface";
14
+ import EmailAutomationEditor from "../forms/EmailAutomationEditor";
15
+
16
+ type EmailAutomationsListProps = {
17
+ /**
18
+ * Pass this whenever the list is the page's own content inside a
19
+ * `RoundPageContainer fullWidth`. Without it ContentListTable wraps itself in
20
+ * `rounded-md border`, drawing a second bordered card inside the page's
21
+ * rounded shell.
22
+ */
23
+ fullWidth?: boolean;
24
+ };
25
+
26
+ export function EmailAutomationsList({ fullWidth }: EmailAutomationsListProps = {}) {
27
+ const t = useTranslations();
28
+ const [triggers, setTriggers] = useState<EmailAutomationTriggerInterface[]>([]);
29
+
30
+ const data: DataListRetriever<EmailAutomationInterface> = useDataListRetriever({
31
+ retriever: (params) => EmailAutomationService.findMany(params),
32
+ retrieverParams: {},
33
+ module: Modules.EmailAutomation,
34
+ });
35
+
36
+ // The trigger column shows the label the app declares, not the stored key.
37
+ useEffect(() => {
38
+ let active = true;
39
+ EmailAutomationService.findTriggers()
40
+ .then((list) => {
41
+ if (active) setTriggers(list);
42
+ })
43
+ .catch(() => {});
44
+ return () => {
45
+ active = false;
46
+ };
47
+ }, []);
48
+
49
+ const triggerLabels = useMemo(
50
+ () => Object.fromEntries(triggers.map((trigger) => [trigger.key, trigger.label])),
51
+ [triggers],
52
+ );
53
+
54
+ const functions: ReactNode[] = [<EmailAutomationEditor key="create-email-automation" />];
55
+
56
+ return (
57
+ <ContentListTable
58
+ data={data}
59
+ fields={[
60
+ EmailAutomationFields.name,
61
+ EmailAutomationFields.trigger,
62
+ EmailAutomationFields.status,
63
+ EmailAutomationFields.steps,
64
+ EmailAutomationFields.active,
65
+ EmailAutomationFields.completed,
66
+ EmailAutomationFields.unsubscribed,
67
+ ]}
68
+ tableGeneratorType={Modules.EmailAutomation}
69
+ functions={functions}
70
+ fullWidth={fullWidth}
71
+ title={t("emailAutomations.admin.title")}
72
+ context={{ triggerLabels }}
73
+ emptyState={<EmptyState icon={MailIcon} title={t("emailAutomations.admin.empty")} />}
74
+ />
75
+ );
76
+ }