@carlonicora/nextjs-jsonapi 6.14.1 → 6.16.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-UIAX72XW.js → BlockNoteEditor-GWDYMS6R.js} +20 -20
- package/dist/{BlockNoteEditor-UIAX72XW.js.map → BlockNoteEditor-GWDYMS6R.js.map} +1 -1
- package/dist/{BlockNoteEditor-I3J6YYXK.mjs → BlockNoteEditor-HNP5UAJT.mjs} +5 -5
- package/dist/{BlockNoteViewer-HIUAJQRI.mjs → BlockNoteViewer-X4UEVI2A.mjs} +3 -3
- package/dist/{BlockNoteViewer-APBOPT4B.js → BlockNoteViewer-XYKFY77F.js} +4 -4
- package/dist/{BlockNoteViewer-APBOPT4B.js.map → BlockNoteViewer-XYKFY77F.js.map} +1 -1
- package/dist/EmailPurpose-DOsSqM5K.d.mts +3 -0
- package/dist/EmailPurpose-DOsSqM5K.d.ts +3 -0
- package/dist/billing/index.js +492 -492
- package/dist/billing/index.mjs +4 -4
- package/dist/{chunk-4XLDBOSE.js → chunk-A7Y5YW7Q.js} +1 -1
- package/dist/{chunk-4XLDBOSE.js.map → chunk-A7Y5YW7Q.js.map} +1 -1
- package/dist/{chunk-ZYSNJKOP.js → chunk-DFA566KY.js} +4 -4
- package/dist/{chunk-ZYSNJKOP.js.map → chunk-DFA566KY.js.map} +1 -1
- package/dist/{chunk-DJXSCARG.js → chunk-I5BQLY7V.js} +9 -9
- package/dist/{chunk-DJXSCARG.js.map → chunk-I5BQLY7V.js.map} +1 -1
- package/dist/{chunk-RFKSJIQB.mjs → chunk-KMPBSOFB.mjs} +138 -5
- package/dist/chunk-KMPBSOFB.mjs.map +1 -0
- package/dist/{chunk-UOTHRJD2.mjs → chunk-OKBJEIOP.mjs} +1 -1
- package/dist/chunk-OKBJEIOP.mjs.map +1 -0
- package/dist/{chunk-PMESKNKL.js → chunk-OO2DBP6L.js} +1106 -973
- package/dist/chunk-OO2DBP6L.js.map +1 -0
- package/dist/{chunk-HAW5PNJG.mjs → chunk-TY5YWBLL.mjs} +3 -3
- package/dist/{chunk-RT7XFGIN.js → chunk-UGDYZW4K.js} +1035 -997
- package/dist/chunk-UGDYZW4K.js.map +1 -0
- package/dist/{chunk-RYY7USSW.mjs → chunk-YEZGASBX.mjs} +50 -12
- package/dist/{chunk-RYY7USSW.mjs.map → chunk-YEZGASBX.mjs.map} +1 -1
- package/dist/{chunk-OUEYZOLB.mjs → chunk-YVKWACDP.mjs} +2 -2
- package/dist/client/index.js +5 -5
- package/dist/client/index.mjs +4 -4
- package/dist/components/editors/server.d.mts +7 -1
- package/dist/components/editors/server.d.ts +7 -1
- package/dist/components/editors/server.js +17 -4
- package/dist/components/editors/server.js.map +1 -1
- package/dist/components/editors/server.mjs +17 -4
- package/dist/components/editors/server.mjs.map +1 -1
- package/dist/components/index.d.mts +3 -2
- package/dist/components/index.d.ts +3 -2
- package/dist/components/index.js +5 -5
- package/dist/components/index.mjs +4 -4
- package/dist/contexts/index.js +5 -5
- package/dist/contexts/index.mjs +4 -4
- package/dist/core/index.d.mts +92 -7
- package/dist/core/index.d.ts +92 -7
- package/dist/core/index.js +7 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +6 -2
- package/dist/{email-automation-enrollment.fields-BZvF14qx.d.mts → email-automation-enrollment.fields-CPsgxLif.d.mts} +11 -1
- package/dist/{email-automation-enrollment.fields-Cp3VMzFq.d.ts → email-automation-enrollment.fields-CVicSamF.d.ts} +11 -1
- package/dist/email-automations/index.d.mts +19 -5
- package/dist/email-automations/index.d.ts +19 -5
- package/dist/email-automations/index.js +356 -129
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +306 -79
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/analytics/index.js +121 -121
- package/dist/features/analytics/index.mjs +4 -4
- package/dist/features/analytics/server.js +5 -5
- package/dist/features/analytics/server.mjs +3 -3
- package/dist/features/blog/index.js +151 -151
- package/dist/features/blog/index.mjs +4 -4
- package/dist/features/blog/server.js +17 -17
- package/dist/features/blog/server.mjs +2 -2
- package/dist/features/help/index.js +38 -38
- package/dist/features/help/index.mjs +4 -4
- package/dist/features/tokenusage/index.js +127 -127
- package/dist/features/tokenusage/index.mjs +4 -4
- package/dist/index.d.mts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7 -3
- package/dist/offline/index.js +14 -14
- package/dist/offline/index.mjs +1 -1
- package/dist/{referral-admin.interface-RVO-ZTJz.d.mts → referral-admin.interface-DTj4LzXx.d.mts} +3 -0
- package/dist/{referral-admin.interface-CnytdDaQ.d.ts → referral-admin.interface-DeSCw3Pp.d.ts} +3 -0
- package/dist/server/index.js +4 -4
- package/dist/server/index.mjs +3 -3
- package/messages/en.json +35 -3
- package/messages/it.json +35 -3
- package/package.json +1 -1
- package/src/components/editors/server/__tests__/renderBlockNoteMarkdown.spec.ts +45 -0
- package/src/components/editors/server/renderBlockNoteHTML.ts +2 -18
- package/src/components/editors/server/renderBlockNoteMarkdown.ts +10 -0
- package/src/components/editors/server/serverEditor.ts +25 -0
- package/src/components/editors/server-entry.ts +1 -0
- package/src/core/index.ts +1 -0
- package/src/core/registry/ModuleRegistry.ts +1 -0
- package/src/features/email-automation/EmailAutomationCandidateModule.ts +12 -0
- package/src/features/email-automation/EmailAutomationModule.ts +1 -0
- package/src/features/email-automation/components/__tests__/EmailAutomationEditor.spec.tsx +74 -9
- package/src/features/email-automation/components/forms/EmailAutomationAddPersonDialog.tsx +226 -0
- package/src/features/email-automation/components/forms/EmailAutomationEditor.tsx +23 -0
- package/src/features/email-automation/components/forms/EmailStepEditor.tsx +7 -3
- package/src/features/email-automation/components/index.ts +1 -0
- package/src/features/email-automation/components/lists/EmailAutomationRecipientsList.tsx +38 -20
- package/src/features/email-automation/components/lists/EmailAutomationsList.tsx +1 -0
- package/src/features/email-automation/components/unsubscribe/EmailUnsubscribe.tsx +9 -4
- package/src/features/email-automation/data/EmailAutomation.ts +9 -0
- package/src/features/email-automation/data/EmailAutomationCandidate.ts +66 -0
- package/src/features/email-automation/data/EmailAutomationCandidateInterface.ts +19 -0
- package/src/features/email-automation/data/EmailAutomationEnrollment.ts +16 -2
- package/src/features/email-automation/data/EmailAutomationEnrollmentInterface.ts +7 -0
- package/src/features/email-automation/data/EmailAutomationInterface.ts +3 -0
- package/src/features/email-automation/data/EmailAutomationService.ts +53 -1
- package/src/features/email-automation/data/EmailPurpose.ts +1 -0
- package/src/features/email-automation/data/__tests__/email-automation-models.spec.ts +70 -0
- package/src/features/email-automation/data/email-automation.fields.ts +1 -0
- package/src/features/email-automation/data/index.ts +2 -0
- package/src/features/email-automation/hooks/useEmailAutomationTableStructure.tsx +19 -0
- package/src/features/email-automation/index.ts +2 -0
- package/src/features/email-blast/EmailBlastModule.ts +1 -0
- package/src/features/email-blast/components/forms/EmailBlastEditor.tsx +33 -4
- package/src/features/email-blast/components/lists/EmailBlastsList.tsx +1 -0
- package/src/features/email-blast/data/EmailBlast.ts +10 -1
- package/src/features/email-blast/data/EmailBlastInterface.ts +3 -0
- package/src/features/email-blast/data/__tests__/EmailBlast.spec.ts +18 -0
- package/src/features/email-blast/data/email-blast.fields.ts +1 -0
- package/src/features/email-blast/hooks/useEmailBlastTableStructure.tsx +15 -0
- package/dist/chunk-PMESKNKL.js.map +0 -1
- package/dist/chunk-RFKSJIQB.mjs.map +0 -1
- package/dist/chunk-RT7XFGIN.js.map +0 -1
- package/dist/chunk-UOTHRJD2.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-I3J6YYXK.mjs.map → BlockNoteEditor-HNP5UAJT.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-HIUAJQRI.mjs.map → BlockNoteViewer-X4UEVI2A.mjs.map} +0 -0
- /package/dist/{chunk-HAW5PNJG.mjs.map → chunk-TY5YWBLL.mjs.map} +0 -0
- /package/dist/{chunk-OUEYZOLB.mjs.map → chunk-YVKWACDP.mjs.map} +0 -0
|
@@ -12,6 +12,7 @@ import { useI18nRouter } from "../../../../i18n";
|
|
|
12
12
|
import { EmailAutomationInput, EmailAutomationInterface } from "../../data/EmailAutomationInterface";
|
|
13
13
|
import { EmailAutomationService } from "../../data/EmailAutomationService";
|
|
14
14
|
import { EmailAutomationTriggerInterface } from "../../data/EmailAutomationTriggerInterface";
|
|
15
|
+
import { EmailPurpose } from "../../data/EmailPurpose";
|
|
15
16
|
|
|
16
17
|
export type EmailAutomationEditorProps = {
|
|
17
18
|
automation?: EmailAutomationInterface;
|
|
@@ -65,6 +66,7 @@ function EmailAutomationEditorInternal({
|
|
|
65
66
|
message: t("emailAutomations.fields.replyTo.error"),
|
|
66
67
|
}),
|
|
67
68
|
isActive: z.boolean(),
|
|
69
|
+
purpose: z.enum(["service", "marketing"], { message: t("emailPurpose.error") }),
|
|
68
70
|
}),
|
|
69
71
|
[t],
|
|
70
72
|
);
|
|
@@ -77,6 +79,8 @@ function EmailAutomationEditorInternal({
|
|
|
77
79
|
exitTrigger: automation?.exitTrigger ?? "",
|
|
78
80
|
replyTo: automation?.replyTo ?? "",
|
|
79
81
|
isActive: automation?.isActive ?? false,
|
|
82
|
+
// No preselected value on create: the form refuses to save without a choice.
|
|
83
|
+
purpose: (automation?.purpose ?? "") as EmailPurpose,
|
|
80
84
|
}),
|
|
81
85
|
[automation],
|
|
82
86
|
);
|
|
@@ -86,6 +90,15 @@ function EmailAutomationEditorInternal({
|
|
|
86
90
|
defaultValues: getDefaultValues(),
|
|
87
91
|
});
|
|
88
92
|
|
|
93
|
+
const purpose = form.watch("purpose");
|
|
94
|
+
const purposeValues = useMemo(
|
|
95
|
+
() => [
|
|
96
|
+
{ id: "service", text: t("emailPurpose.service") },
|
|
97
|
+
{ id: "marketing", text: t("emailPurpose.marketing") },
|
|
98
|
+
],
|
|
99
|
+
[t],
|
|
100
|
+
);
|
|
101
|
+
|
|
89
102
|
return (
|
|
90
103
|
<EditorSheet
|
|
91
104
|
form={form}
|
|
@@ -106,6 +119,7 @@ function EmailAutomationEditorInternal({
|
|
|
106
119
|
exitTrigger: values.exitTrigger || undefined,
|
|
107
120
|
replyTo: values.replyTo || undefined,
|
|
108
121
|
isActive: values.isActive,
|
|
122
|
+
purpose: values.purpose,
|
|
109
123
|
};
|
|
110
124
|
return isEdit ? await EmailAutomationService.update(payload) : await EmailAutomationService.create(payload);
|
|
111
125
|
}}
|
|
@@ -118,6 +132,15 @@ function EmailAutomationEditorInternal({
|
|
|
118
132
|
>
|
|
119
133
|
<div className="flex w-full flex-col gap-y-4">
|
|
120
134
|
<FormInput form={form} id="name" name={t("emailAutomations.fields.name.label")} isRequired />
|
|
135
|
+
<FormSelect
|
|
136
|
+
form={form}
|
|
137
|
+
id="purpose"
|
|
138
|
+
name={t("emailPurpose.label")}
|
|
139
|
+
placeholder={t("emailPurpose.placeholder")}
|
|
140
|
+
values={purposeValues}
|
|
141
|
+
description={purpose === "service" ? t("emailPurpose.serviceWarning") : undefined}
|
|
142
|
+
isRequired
|
|
143
|
+
/>
|
|
121
144
|
<FormSelect
|
|
122
145
|
form={form}
|
|
123
146
|
id="trigger"
|
|
@@ -13,7 +13,9 @@ import { EmailAutomationStepInput, EmailAutomationStepInterface } from "../../da
|
|
|
13
13
|
import { EmailAutomationStepService } from "../../data/EmailAutomationStepService";
|
|
14
14
|
|
|
15
15
|
/** The fixed variable set the renderer fills (spec § "Rendering"). */
|
|
16
|
-
const TEMPLATE_VARIABLES = ["{{name}}", "{{email}}", "{{url}}", "{{unsubscribeUrl}}"
|
|
16
|
+
const TEMPLATE_VARIABLES = ["{{name}}", "{{email}}", "{{url}}", "{{unsubscribeUrl}}"];
|
|
17
|
+
/** Only a Marketing automation fills it: a Service email has no "stop all marketing emails" link. */
|
|
18
|
+
const MARKETING_ONLY_VARIABLES = ["{{unsubscribeAllUrl}}"];
|
|
17
19
|
|
|
18
20
|
export type EmailStepEditorProps = {
|
|
19
21
|
step?: EmailAutomationStepInterface;
|
|
@@ -43,8 +45,10 @@ function EmailStepEditorInternal({
|
|
|
43
45
|
onSuccess,
|
|
44
46
|
}: EmailStepEditorProps) {
|
|
45
47
|
const t = useTranslations();
|
|
46
|
-
const { reload } = useEmailAutomation();
|
|
48
|
+
const { automation, reload } = useEmailAutomation();
|
|
47
49
|
const isEdit = !!step;
|
|
50
|
+
const templateVariables =
|
|
51
|
+
automation?.purpose === "service" ? TEMPLATE_VARIABLES : [...TEMPLATE_VARIABLES, ...MARKETING_ONLY_VARIABLES];
|
|
48
52
|
|
|
49
53
|
const formSchema = useMemo(
|
|
50
54
|
() =>
|
|
@@ -129,7 +133,7 @@ function EmailStepEditorInternal({
|
|
|
129
133
|
<>
|
|
130
134
|
<span className="block">
|
|
131
135
|
{t("emailAutomations.step.variablesHint")}{" "}
|
|
132
|
-
{
|
|
136
|
+
{templateVariables.map((variable) => (
|
|
133
137
|
<span key={variable} className="me-2 text-foreground">
|
|
134
138
|
{variable}
|
|
135
139
|
</span>
|
|
@@ -4,6 +4,7 @@ export * from "./lists/EmailAutomationsList";
|
|
|
4
4
|
export * from "./lists/EmailAutomationRecipientsList";
|
|
5
5
|
export * from "./forms/EmailAutomationEditor";
|
|
6
6
|
export * from "./forms/EmailStepEditor";
|
|
7
|
+
export * from "./forms/EmailAutomationAddPersonDialog";
|
|
7
8
|
export * from "./canvas/EmailAutomationActiveSwitch";
|
|
8
9
|
export * from "./canvas/EmailAutomationCanvasTab";
|
|
9
10
|
export * from "./canvas/EmailAutomationCanvas";
|
|
@@ -8,18 +8,20 @@ import { Modules } from "../../../../core";
|
|
|
8
8
|
import { DataListRetriever, useDataListRetriever } from "../../../../hooks";
|
|
9
9
|
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../shadcnui";
|
|
10
10
|
import { useEmailAutomation } from "../../contexts/EmailAutomationContext";
|
|
11
|
+
import { EmailAutomationAddPersonDialog } from "../forms/EmailAutomationAddPersonDialog";
|
|
11
12
|
import { EmailAutomationEnrollmentFields } from "../../data/email-automation-enrollment.fields";
|
|
12
13
|
import { EmailAutomationEnrollmentInterface } from "../../data/EmailAutomationEnrollmentInterface";
|
|
13
14
|
import { EmailAutomationService } from "../../data/EmailAutomationService";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `fullWidth`), as PricesList does
|
|
17
|
+
* List of the people in this automation (owner decision 6: no row actions; a
|
|
18
|
+
* person is added by hand through "Add person" in the filters). Rendered inside
|
|
19
|
+
* a tab, so it keeps its own bordered card (no `fullWidth`), as PricesList does
|
|
20
|
+
* inside ProductContainer.
|
|
19
21
|
*/
|
|
20
22
|
export function EmailAutomationRecipientsList() {
|
|
21
23
|
const t = useTranslations();
|
|
22
|
-
const { automation } = useEmailAutomation();
|
|
24
|
+
const { automation, reload } = useEmailAutomation();
|
|
23
25
|
const [statusFilter, setStatusFilter] = useState<string>("all");
|
|
24
26
|
|
|
25
27
|
const data: DataListRetriever<EmailAutomationEnrollmentInterface> = useDataListRetriever({
|
|
@@ -48,6 +50,17 @@ export function EmailAutomationRecipientsList() {
|
|
|
48
50
|
|
|
49
51
|
const filters = (
|
|
50
52
|
<div className="flex items-center gap-4">
|
|
53
|
+
{automation && (
|
|
54
|
+
<EmailAutomationAddPersonDialog
|
|
55
|
+
automationId={automation.id}
|
|
56
|
+
disabled={automation.stepCount === 0}
|
|
57
|
+
onAdded={() => {
|
|
58
|
+
void data.refresh();
|
|
59
|
+
void reload();
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
|
|
51
64
|
<Select items={statusItems} value={statusFilter} onValueChange={handleStatusChange}>
|
|
52
65
|
<SelectTrigger className="w-40">
|
|
53
66
|
<SelectValue />
|
|
@@ -69,22 +82,27 @@ export function EmailAutomationRecipientsList() {
|
|
|
69
82
|
);
|
|
70
83
|
|
|
71
84
|
return (
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
<div data-testid="email-automation-recipients">
|
|
86
|
+
<ContentListTable
|
|
87
|
+
data={data}
|
|
88
|
+
fields={[
|
|
89
|
+
EmailAutomationEnrollmentFields.email,
|
|
90
|
+
EmailAutomationEnrollmentFields.name,
|
|
91
|
+
EmailAutomationEnrollmentFields.status,
|
|
92
|
+
EmailAutomationEnrollmentFields.currentStep,
|
|
93
|
+
EmailAutomationEnrollmentFields.nextSendAt,
|
|
94
|
+
EmailAutomationEnrollmentFields.sentCount,
|
|
95
|
+
]}
|
|
96
|
+
tableGeneratorType={Modules.EmailAutomationEnrollment}
|
|
97
|
+
/* ContentListTable renders its filters only inside the title bar, so the
|
|
98
|
+
list needs a title for "Add person" and the status filter to show. */
|
|
99
|
+
title={t("emailAutomations.tabs.recipients")}
|
|
100
|
+
filters={filters}
|
|
101
|
+
/* The enrolments endpoint takes no search term, so rendering the box would
|
|
85
102
|
give the page a control that silently does nothing. */
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
allowSearch={false}
|
|
104
|
+
emptyState={t("emailAutomations.recipients.empty")}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
89
107
|
);
|
|
90
108
|
}
|
|
@@ -58,6 +58,7 @@ export function EmailAutomationsList({ fullWidth }: EmailAutomationsListProps =
|
|
|
58
58
|
data={data}
|
|
59
59
|
fields={[
|
|
60
60
|
EmailAutomationFields.name,
|
|
61
|
+
EmailAutomationFields.purpose,
|
|
61
62
|
EmailAutomationFields.trigger,
|
|
62
63
|
EmailAutomationFields.status,
|
|
63
64
|
EmailAutomationFields.steps,
|
|
@@ -27,6 +27,8 @@ export function EmailUnsubscribe({ token, scope, source = "automation" }: Props)
|
|
|
27
27
|
const [state, setState] = useState<UnsubscribeState>("loading");
|
|
28
28
|
const [stoppedScope, setStoppedScope] = useState<UnsubscribeScope>(scope);
|
|
29
29
|
const [automationName, setAutomationName] = useState<string>("");
|
|
30
|
+
// A Service automation is not a marketing email: its page offers only "Stop these emails".
|
|
31
|
+
const [isServiceAutomation, setIsServiceAutomation] = useState<boolean>(false);
|
|
30
32
|
const [isStoppingAll, setIsStoppingAll] = useState<boolean>(false);
|
|
31
33
|
|
|
32
34
|
useEffect(() => {
|
|
@@ -40,6 +42,7 @@ export function EmailUnsubscribe({ token, scope, source = "automation" }: Props)
|
|
|
40
42
|
}
|
|
41
43
|
const enrollment = await EmailAutomationEnrollmentService.unsubscribe({ token, scope });
|
|
42
44
|
setAutomationName(enrollment?.automation?.name ?? "");
|
|
45
|
+
setIsServiceAutomation(enrollment?.automation?.purpose === "service");
|
|
43
46
|
setStoppedScope(scope);
|
|
44
47
|
setState("success");
|
|
45
48
|
} catch {
|
|
@@ -100,10 +103,12 @@ export function EmailUnsubscribe({ token, scope, source = "automation" }: Props)
|
|
|
100
103
|
<p className="text-muted-foreground max-w-md text-sm leading-relaxed">
|
|
101
104
|
{t("emailAutomations.unsubscribe.stoppedAutomation", { name: automationName })}
|
|
102
105
|
</p>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
{!isServiceAutomation && (
|
|
107
|
+
<Button variant="secondary" onClick={stopAll} disabled={isStoppingAll}>
|
|
108
|
+
{isStoppingAll && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
109
|
+
{t("emailAutomations.unsubscribe.alsoStopAll")}
|
|
110
|
+
</Button>
|
|
111
|
+
)}
|
|
107
112
|
</>
|
|
108
113
|
)}
|
|
109
114
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
2
|
import { EmailAutomationInput, EmailAutomationInterface } from "./EmailAutomationInterface";
|
|
3
|
+
import { EmailPurpose } from "./EmailPurpose";
|
|
3
4
|
|
|
4
5
|
export class EmailAutomation extends AbstractApiData implements EmailAutomationInterface {
|
|
5
6
|
private _name?: string;
|
|
@@ -7,6 +8,7 @@ export class EmailAutomation extends AbstractApiData implements EmailAutomationI
|
|
|
7
8
|
private _exitTrigger?: string;
|
|
8
9
|
private _replyTo?: string;
|
|
9
10
|
private _isActive: boolean = false;
|
|
11
|
+
private _purpose: EmailPurpose = "marketing";
|
|
10
12
|
private _stepCount?: number;
|
|
11
13
|
private _activeCount?: number;
|
|
12
14
|
private _completedCount?: number;
|
|
@@ -32,6 +34,10 @@ export class EmailAutomation extends AbstractApiData implements EmailAutomationI
|
|
|
32
34
|
return this._isActive;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
get purpose(): EmailPurpose {
|
|
38
|
+
return this._purpose;
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
get stepCount(): number {
|
|
36
42
|
return this._stepCount ?? 0;
|
|
37
43
|
}
|
|
@@ -56,6 +62,8 @@ export class EmailAutomation extends AbstractApiData implements EmailAutomationI
|
|
|
56
62
|
this._exitTrigger = data.jsonApi.attributes.exitTrigger ?? undefined;
|
|
57
63
|
this._replyTo = data.jsonApi.attributes.replyTo ?? undefined;
|
|
58
64
|
this._isActive = data.jsonApi.attributes.isActive === true;
|
|
65
|
+
// A record saved before purpose existed counts as Marketing.
|
|
66
|
+
this._purpose = data.jsonApi.attributes.purpose === "service" ? "service" : "marketing";
|
|
59
67
|
this._stepCount = data.jsonApi.attributes.stepCount;
|
|
60
68
|
this._activeCount = data.jsonApi.attributes.activeCount;
|
|
61
69
|
this._completedCount = data.jsonApi.attributes.completedCount;
|
|
@@ -81,6 +89,7 @@ export class EmailAutomation extends AbstractApiData implements EmailAutomationI
|
|
|
81
89
|
if (data.exitTrigger !== undefined) response.data.attributes.exitTrigger = data.exitTrigger;
|
|
82
90
|
if (data.replyTo !== undefined) response.data.attributes.replyTo = data.replyTo;
|
|
83
91
|
if (data.isActive !== undefined) response.data.attributes.isActive = data.isActive;
|
|
92
|
+
if (data.purpose !== undefined) response.data.attributes.purpose = data.purpose;
|
|
84
93
|
|
|
85
94
|
return response;
|
|
86
95
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import {
|
|
3
|
+
EmailAutomationCandidateInterface,
|
|
4
|
+
EmailAutomationCandidateReason,
|
|
5
|
+
EmailAutomationRecipientType,
|
|
6
|
+
} from "./EmailAutomationCandidateInterface";
|
|
7
|
+
|
|
8
|
+
export class EmailAutomationCandidate extends AbstractApiData implements EmailAutomationCandidateInterface {
|
|
9
|
+
private _recipientType: EmailAutomationRecipientType = "user";
|
|
10
|
+
private _recipientId?: string;
|
|
11
|
+
private _email?: string;
|
|
12
|
+
private _name?: string;
|
|
13
|
+
private _addable?: boolean;
|
|
14
|
+
private _reason?: EmailAutomationCandidateReason;
|
|
15
|
+
|
|
16
|
+
get recipientType(): EmailAutomationRecipientType {
|
|
17
|
+
return this._recipientType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get recipientId(): string {
|
|
21
|
+
return this._recipientId ?? "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get email(): string {
|
|
25
|
+
return this._email ?? "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get name(): string | undefined {
|
|
29
|
+
return this._name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get addable(): boolean {
|
|
33
|
+
return this._addable ?? false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get reason(): EmailAutomationCandidateReason | undefined {
|
|
37
|
+
return this._reason;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
41
|
+
super.rehydrate(data);
|
|
42
|
+
|
|
43
|
+
const attributes = data.jsonApi.attributes;
|
|
44
|
+
this._recipientType = attributes.recipientType === "waitlist" ? "waitlist" : "user";
|
|
45
|
+
this._recipientId = attributes.recipientId;
|
|
46
|
+
this._email = attributes.email;
|
|
47
|
+
this._name = attributes.name ?? undefined;
|
|
48
|
+
this._addable = attributes.addable === true;
|
|
49
|
+
this._reason = attributes.reason ?? undefined;
|
|
50
|
+
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The API computes candidates and never accepts one as a request body; this
|
|
56
|
+
* only identifies the resource.
|
|
57
|
+
*/
|
|
58
|
+
createJsonApi(data: { id: string }) {
|
|
59
|
+
return {
|
|
60
|
+
data: {
|
|
61
|
+
type: Modules.EmailAutomationCandidate.name,
|
|
62
|
+
id: data.id,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export type EmailAutomationRecipientType = "user" | "waitlist";
|
|
4
|
+
|
|
5
|
+
export type EmailAutomationCandidateReason = "already_enrolled" | "opted_out" | "no_consent";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A person the administrator may add to an automation by hand. The API decides
|
|
9
|
+
* whether they can be added (`addable`) and why not (`reason`); the browser only
|
|
10
|
+
* displays it.
|
|
11
|
+
*/
|
|
12
|
+
export interface EmailAutomationCandidateInterface extends ApiDataInterface {
|
|
13
|
+
get recipientType(): EmailAutomationRecipientType;
|
|
14
|
+
get recipientId(): string;
|
|
15
|
+
get email(): string;
|
|
16
|
+
get name(): string | undefined;
|
|
17
|
+
get addable(): boolean;
|
|
18
|
+
get reason(): EmailAutomationCandidateReason | undefined;
|
|
19
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
2
|
import {
|
|
3
|
+
EmailAutomationEnrollmentInput,
|
|
3
4
|
EmailAutomationEnrollmentInterface,
|
|
4
5
|
EmailAutomationEnrollmentStatus,
|
|
5
6
|
} from "./EmailAutomationEnrollmentInterface";
|
|
@@ -85,8 +86,21 @@ export class EmailAutomationEnrollment extends AbstractApiData implements EmailA
|
|
|
85
86
|
return this;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Body of POST email-automations/:id/email-automation-enrollments: an
|
|
91
|
+
* administrator adds a person by hand. The API enrols them at the first step.
|
|
92
|
+
*/
|
|
93
|
+
createJsonApi(data: EmailAutomationEnrollmentInput) {
|
|
94
|
+
return {
|
|
95
|
+
data: {
|
|
96
|
+
type: Modules.EmailAutomationEnrollment.name,
|
|
97
|
+
id: data.id,
|
|
98
|
+
attributes: {
|
|
99
|
+
recipientType: data.recipientType,
|
|
100
|
+
recipientId: data.recipientId,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
/**
|
|
@@ -3,6 +3,13 @@ import { EmailAutomationInterface } from "./EmailAutomationInterface";
|
|
|
3
3
|
|
|
4
4
|
export type EmailAutomationEnrollmentStatus = "active" | "completed" | "unsubscribed" | "exited" | "failed";
|
|
5
5
|
|
|
6
|
+
/** Body of POST email-automations/:id/email-automation-enrollments (manual add). */
|
|
7
|
+
export type EmailAutomationEnrollmentInput = {
|
|
8
|
+
id: string;
|
|
9
|
+
recipientType: "user" | "waitlist";
|
|
10
|
+
recipientId: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
6
13
|
export interface EmailAutomationEnrollmentInterface extends ApiDataInterface {
|
|
7
14
|
get email(): string;
|
|
8
15
|
get name(): string | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
import { EmailPurpose } from "./EmailPurpose";
|
|
2
3
|
|
|
3
4
|
export type EmailAutomationInput = {
|
|
4
5
|
id: string;
|
|
@@ -7,6 +8,7 @@ export type EmailAutomationInput = {
|
|
|
7
8
|
exitTrigger?: string;
|
|
8
9
|
replyTo?: string;
|
|
9
10
|
isActive?: boolean;
|
|
11
|
+
purpose?: EmailPurpose;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export interface EmailAutomationInterface extends ApiDataInterface {
|
|
@@ -15,6 +17,7 @@ export interface EmailAutomationInterface extends ApiDataInterface {
|
|
|
15
17
|
get exitTrigger(): string | undefined;
|
|
16
18
|
get replyTo(): string | undefined;
|
|
17
19
|
get isActive(): boolean;
|
|
20
|
+
get purpose(): EmailPurpose;
|
|
18
21
|
get stepCount(): number;
|
|
19
22
|
get activeCount(): number;
|
|
20
23
|
get completedCount(): number;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { AbstractService, EndpointCreator, HttpMethod, Modules, NextRef, PreviousRef } from "../../../core";
|
|
2
|
-
import {
|
|
2
|
+
import { EmailAutomationCandidateInterface } from "./EmailAutomationCandidateInterface";
|
|
3
|
+
import {
|
|
4
|
+
EmailAutomationEnrollmentInput,
|
|
5
|
+
EmailAutomationEnrollmentInterface,
|
|
6
|
+
} from "./EmailAutomationEnrollmentInterface";
|
|
3
7
|
import { EmailAutomationInput, EmailAutomationInterface } from "./EmailAutomationInterface";
|
|
4
8
|
import { EmailAutomationTriggerInterface } from "./EmailAutomationTriggerInterface";
|
|
5
9
|
|
|
@@ -103,4 +107,52 @@ export class EmailAutomationService extends AbstractService {
|
|
|
103
107
|
previous: params.previous,
|
|
104
108
|
});
|
|
105
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* GET email-automations/:id/email-automation-candidates: users and waitlist
|
|
113
|
+
* entries matching the search, each flagged addable or not by the API. Paged:
|
|
114
|
+
* read through useDataListRetriever, which passes the next/previous refs.
|
|
115
|
+
*/
|
|
116
|
+
static async findCandidates(params: {
|
|
117
|
+
automationId: string;
|
|
118
|
+
search?: string;
|
|
119
|
+
next?: NextRef;
|
|
120
|
+
previous?: PreviousRef;
|
|
121
|
+
}): Promise<EmailAutomationCandidateInterface[]> {
|
|
122
|
+
const endpoint = new EndpointCreator({
|
|
123
|
+
endpoint: Modules.EmailAutomation,
|
|
124
|
+
id: params.automationId,
|
|
125
|
+
childEndpoint: Modules.EmailAutomationCandidate,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (params.search) endpoint.addAdditionalParam("search", params.search);
|
|
129
|
+
|
|
130
|
+
return this.callApi<EmailAutomationCandidateInterface[]>({
|
|
131
|
+
type: Modules.EmailAutomationCandidate,
|
|
132
|
+
method: HttpMethod.GET,
|
|
133
|
+
endpoint: endpoint.generate(),
|
|
134
|
+
next: params.next,
|
|
135
|
+
previous: params.previous,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* POST email-automations/:id/email-automation-enrollments: adds one person by
|
|
141
|
+
* hand. The API re-checks every rule and answers 400 with the reason.
|
|
142
|
+
*/
|
|
143
|
+
static async addEnrollment(params: {
|
|
144
|
+
automationId: string;
|
|
145
|
+
input: EmailAutomationEnrollmentInput;
|
|
146
|
+
}): Promise<EmailAutomationEnrollmentInterface> {
|
|
147
|
+
return this.callApi<EmailAutomationEnrollmentInterface>({
|
|
148
|
+
type: Modules.EmailAutomationEnrollment,
|
|
149
|
+
method: HttpMethod.POST,
|
|
150
|
+
endpoint: new EndpointCreator({
|
|
151
|
+
endpoint: Modules.EmailAutomation,
|
|
152
|
+
id: params.automationId,
|
|
153
|
+
childEndpoint: Modules.EmailAutomationEnrollment,
|
|
154
|
+
}).generate(),
|
|
155
|
+
input: params.input,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
106
158
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type EmailPurpose = "service" | "marketing";
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
2
|
import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
|
|
3
3
|
import { ModuleFactory, ModuleWithPermissions } from "../../../../permissions/types";
|
|
4
|
+
import { EmailAutomationCandidateModule } from "../../EmailAutomationCandidateModule";
|
|
4
5
|
import { EmailAutomationEnrollmentModule } from "../../EmailAutomationEnrollmentModule";
|
|
5
6
|
import { EmailAutomationModule } from "../../EmailAutomationModule";
|
|
6
7
|
import { EmailAutomationStepModule } from "../../EmailAutomationStepModule";
|
|
7
8
|
import { EmailAutomationTriggerModule } from "../../EmailAutomationTriggerModule";
|
|
8
9
|
import { EmailAutomation } from "../EmailAutomation";
|
|
10
|
+
import { EmailAutomationCandidate } from "../EmailAutomationCandidate";
|
|
9
11
|
import { EMAIL_AUTOMATION_UNSUBSCRIBE_TYPE, EmailAutomationEnrollment } from "../EmailAutomationEnrollment";
|
|
10
12
|
import { EmailAutomationStep } from "../EmailAutomationStep";
|
|
11
13
|
|
|
@@ -24,6 +26,7 @@ beforeAll(() => {
|
|
|
24
26
|
ModuleRegistry.register("EmailAutomationStep", EmailAutomationStepModule(moduleFactory));
|
|
25
27
|
ModuleRegistry.register("EmailAutomationEnrollment", EmailAutomationEnrollmentModule(moduleFactory));
|
|
26
28
|
ModuleRegistry.register("EmailAutomationTrigger", EmailAutomationTriggerModule(moduleFactory));
|
|
29
|
+
ModuleRegistry.register("EmailAutomationCandidate", EmailAutomationCandidateModule(moduleFactory));
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
const hydrate = (attributes: Record<string, unknown>) =>
|
|
@@ -100,4 +103,71 @@ describe("email automation models", () => {
|
|
|
100
103
|
data: { type: EMAIL_AUTOMATION_UNSUBSCRIBE_TYPE, attributes: { scope: "all" } },
|
|
101
104
|
});
|
|
102
105
|
});
|
|
106
|
+
|
|
107
|
+
it("EmailAutomationCandidate rehydrates kind, id, email, addable and reason", () => {
|
|
108
|
+
const blocked = new EmailAutomationCandidate().rehydrate(
|
|
109
|
+
hydrate({
|
|
110
|
+
recipientType: "waitlist",
|
|
111
|
+
recipientId: "w1",
|
|
112
|
+
email: "ada@example.com",
|
|
113
|
+
addable: false,
|
|
114
|
+
reason: "no_consent",
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
expect(blocked.recipientType).toBe("waitlist");
|
|
118
|
+
expect(blocked.recipientId).toBe("w1");
|
|
119
|
+
expect(blocked.email).toBe("ada@example.com");
|
|
120
|
+
expect(blocked.name).toBeUndefined();
|
|
121
|
+
expect(blocked.addable).toBe(false);
|
|
122
|
+
expect(blocked.reason).toBe("no_consent");
|
|
123
|
+
|
|
124
|
+
const open = new EmailAutomationCandidate().rehydrate(
|
|
125
|
+
hydrate({
|
|
126
|
+
recipientType: "user",
|
|
127
|
+
recipientId: "u1",
|
|
128
|
+
email: "grace@example.com",
|
|
129
|
+
name: "Grace",
|
|
130
|
+
addable: true,
|
|
131
|
+
reason: null,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
expect(open.recipientType).toBe("user");
|
|
135
|
+
expect(open.name).toBe("Grace");
|
|
136
|
+
expect(open.addable).toBe(true);
|
|
137
|
+
expect(open.reason).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("EmailAutomationCandidate createJsonApi returns type and id only", () => {
|
|
141
|
+
expect(new EmailAutomationCandidate().createJsonApi({ id: "u1" })).toEqual({
|
|
142
|
+
data: { type: "email-automation-candidates", id: "u1" },
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("EmailAutomationEnrollment.createJsonApi builds type, id, recipientType and recipientId", () => {
|
|
147
|
+
expect(
|
|
148
|
+
new EmailAutomationEnrollment().createJsonApi({ id: "e1", recipientType: "user", recipientId: "u1" }),
|
|
149
|
+
).toEqual({
|
|
150
|
+
data: {
|
|
151
|
+
type: "email-automation-enrollments",
|
|
152
|
+
id: "e1",
|
|
153
|
+
attributes: { recipientType: "user", recipientId: "u1" },
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
it('EmailAutomation rehydrates purpose "service"', () => {
|
|
158
|
+
const m = new EmailAutomation().rehydrate(hydrate({ name: "Welcome", purpose: "service" }));
|
|
159
|
+
expect(m.purpose).toBe("service");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("missing purpose rehydrates as marketing", () => {
|
|
163
|
+
expect(new EmailAutomation().rehydrate(hydrate({ name: "Welcome" })).purpose).toBe("marketing");
|
|
164
|
+
expect(new EmailAutomation().rehydrate(hydrate({ name: "Welcome", purpose: "other" })).purpose).toBe("marketing");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("createJsonApi sends purpose when set", () => {
|
|
168
|
+
const body = new EmailAutomation().createJsonApi({ id: "a1", name: "Welcome", purpose: "service" });
|
|
169
|
+
expect(body.data.attributes.purpose).toBe("service");
|
|
170
|
+
const without = new EmailAutomation().createJsonApi({ id: "a1", name: "Welcome" });
|
|
171
|
+
expect("purpose" in without.data.attributes).toBe(false);
|
|
172
|
+
});
|
|
103
173
|
});
|
|
@@ -9,5 +9,7 @@ export * from "./EmailAutomationEnrollment";
|
|
|
9
9
|
export * from "./EmailAutomationEnrollmentService";
|
|
10
10
|
export * from "./EmailAutomationTriggerInterface";
|
|
11
11
|
export * from "./EmailAutomationTrigger";
|
|
12
|
+
export * from "./EmailAutomationCandidateInterface";
|
|
13
|
+
export * from "./EmailAutomationCandidate";
|
|
12
14
|
export * from "./email-automation.fields";
|
|
13
15
|
export * from "./email-automation-enrollment.fields";
|
|
@@ -62,6 +62,25 @@ export const useEmailAutomationTableStructure: UseTableStructureHook<
|
|
|
62
62
|
enableSorting: false,
|
|
63
63
|
enableHiding: false,
|
|
64
64
|
}),
|
|
65
|
+
[EmailAutomationFields.purpose]: () => ({
|
|
66
|
+
id: "purpose",
|
|
67
|
+
accessorKey: "purpose",
|
|
68
|
+
header: t("emailAutomations.list.columns.purpose"),
|
|
69
|
+
cell: ({ row }: { row: TableContent<EmailAutomationInterface> }) => {
|
|
70
|
+
const automation: EmailAutomationInterface = row.original.jsonApiData;
|
|
71
|
+
return automation.purpose === "service" ? (
|
|
72
|
+
<Badge variant="softBlue" data-testid="email-automation-purpose-badge">
|
|
73
|
+
{t("emailPurpose.service")}
|
|
74
|
+
</Badge>
|
|
75
|
+
) : (
|
|
76
|
+
<Badge variant="softGray" data-testid="email-automation-purpose-badge">
|
|
77
|
+
{t("emailPurpose.marketing")}
|
|
78
|
+
</Badge>
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
enableSorting: false,
|
|
82
|
+
enableHiding: false,
|
|
83
|
+
}),
|
|
65
84
|
[EmailAutomationFields.trigger]: () => ({
|
|
66
85
|
id: "trigger",
|
|
67
86
|
accessorKey: "trigger",
|
|
@@ -2,4 +2,6 @@ export * from "./EmailAutomationModule";
|
|
|
2
2
|
export * from "./EmailAutomationStepModule";
|
|
3
3
|
export * from "./EmailAutomationEnrollmentModule";
|
|
4
4
|
export * from "./EmailAutomationTriggerModule";
|
|
5
|
+
export * from "./EmailAutomationCandidateModule";
|
|
5
6
|
export * from "./data";
|
|
7
|
+
export * from "./data/EmailPurpose";
|