@aglyn/plugins-email 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +58 -0
- package/src/index.d.ts +35 -0
- package/src/index.js +35 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/campaign-design-create-widget.d.ts +21 -0
- package/src/lib/components/campaign-design-create-widget.js +61 -0
- package/src/lib/components/campaign-design-create-widget.js.map +1 -0
- package/src/lib/components/campaign-sender-editor-widget.d.ts +18 -0
- package/src/lib/components/campaign-sender-editor-widget.js +39 -0
- package/src/lib/components/campaign-sender-editor-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-options-widget.d.ts +19 -0
- package/src/lib/components/campaign-topic-options-widget.js +48 -0
- package/src/lib/components/campaign-topic-options-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-select.d.ts +32 -0
- package/src/lib/components/campaign-topic-select.js +87 -0
- package/src/lib/components/campaign-topic-select.js.map +1 -0
- package/src/lib/components/dynamic-list-rule-fields.d.ts +173 -0
- package/src/lib/components/dynamic-list-rule-fields.js +1473 -0
- package/src/lib/components/dynamic-list-rule-fields.js.map +1 -0
- package/src/lib/components/email-blocks.d.ts +111 -0
- package/src/lib/components/email-blocks.js +875 -0
- package/src/lib/components/email-blocks.js.map +1 -0
- package/src/lib/components/email-design-preview.d.ts +62 -0
- package/src/lib/components/email-design-preview.js +174 -0
- package/src/lib/components/email-design-preview.js.map +1 -0
- package/src/lib/components/email-screens-card.d.ts +42 -0
- package/src/lib/components/email-screens-card.js +277 -0
- package/src/lib/components/email-screens-card.js.map +1 -0
- package/src/lib/components/email-template-detail.d.ts +48 -0
- package/src/lib/components/email-template-detail.js +681 -0
- package/src/lib/components/email-template-detail.js.map +1 -0
- package/src/lib/components/email-topic-detail.d.ts +32 -0
- package/src/lib/components/email-topic-detail.js +293 -0
- package/src/lib/components/email-topic-detail.js.map +1 -0
- package/src/lib/components/email-topics-card.d.ts +46 -0
- package/src/lib/components/email-topics-card.js +327 -0
- package/src/lib/components/email-topics-card.js.map +1 -0
- package/src/lib/components/email-zones.d.ts +28 -0
- package/src/lib/components/email-zones.js +20 -0
- package/src/lib/components/email-zones.js.map +1 -0
- package/src/lib/components/emails-console-page.d.ts +32 -0
- package/src/lib/components/emails-console-page.js +229 -0
- package/src/lib/components/emails-console-page.js.map +1 -0
- package/src/lib/components/emails-console-sections.d.ts +36 -0
- package/src/lib/components/emails-console-sections.js +108 -0
- package/src/lib/components/emails-console-sections.js.map +1 -0
- package/src/lib/components/list-detail-card.d.ts +47 -0
- package/src/lib/components/list-detail-card.js +273 -0
- package/src/lib/components/list-detail-card.js.map +1 -0
- package/src/lib/components/list-edit-card.d.ts +11 -0
- package/src/lib/components/list-edit-card.js +287 -0
- package/src/lib/components/list-edit-card.js.map +1 -0
- package/src/lib/components/list-import-drawer.d.ts +22 -0
- package/src/lib/components/list-import-drawer.js +662 -0
- package/src/lib/components/list-import-drawer.js.map +1 -0
- package/src/lib/components/list-members-panel.d.ts +94 -0
- package/src/lib/components/list-members-panel.js +686 -0
- package/src/lib/components/list-members-panel.js.map +1 -0
- package/src/lib/components/lists-card.d.ts +28 -0
- package/src/lib/components/lists-card.js +377 -0
- package/src/lib/components/lists-card.js.map +1 -0
- package/src/lib/components/sending-domain-detail.d.ts +26 -0
- package/src/lib/components/sending-domain-detail.js +496 -0
- package/src/lib/components/sending-domain-detail.js.map +1 -0
- package/src/lib/components/sending-domains-card.d.ts +33 -0
- package/src/lib/components/sending-domains-card.js +962 -0
- package/src/lib/components/sending-domains-card.js.map +1 -0
- package/src/lib/components/sending-sender-drawer.d.ts +94 -0
- package/src/lib/components/sending-sender-drawer.js +543 -0
- package/src/lib/components/sending-sender-drawer.js.map +1 -0
- package/src/lib/components/suppressions-card.d.ts +49 -0
- package/src/lib/components/suppressions-card.js +639 -0
- package/src/lib/components/suppressions-card.js.map +1 -0
- package/src/lib/components/use-org-email-topics.d.ts +79 -0
- package/src/lib/components/use-org-email-topics.js +111 -0
- package/src/lib/components/use-org-email-topics.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +18 -0
- package/src/lib/constants/bundle-common.js +18 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-org-company-options.d.ts +20 -0
- package/src/lib/hooks/use-org-company-options.js +138 -0
- package/src/lib/hooks/use-org-company-options.js.map +1 -0
- package/src/lib/hooks/use-org-contact-fields.d.ts +40 -0
- package/src/lib/hooks/use-org-contact-fields.js +91 -0
- package/src/lib/hooks/use-org-contact-fields.js.map +1 -0
- package/src/lib/hooks/use-org-contact-segments.d.ts +16 -0
- package/src/lib/hooks/use-org-contact-segments.js +55 -0
- package/src/lib/hooks/use-org-contact-segments.js.map +1 -0
- package/src/lib/hooks/use-org-crm-views.d.ts +8 -0
- package/src/lib/hooks/use-org-crm-views.js +74 -0
- package/src/lib/hooks/use-org-crm-views.js.map +1 -0
- package/src/lib/hooks/use-org-lists.d.ts +8 -0
- package/src/lib/hooks/use-org-lists.js +47 -0
- package/src/lib/hooks/use-org-lists.js.map +1 -0
- package/src/lib/model/email-design-document.d.ts +52 -0
- package/src/lib/model/email-design-document.js +62 -0
- package/src/lib/model/email-design-document.js.map +1 -0
- package/src/lib/model/index.d.ts +64 -0
- package/src/lib/model/index.js +71 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/sending-domain-status.d.ts +99 -0
- package/src/lib/model/sending-domain-status.js +196 -0
- package/src/lib/model/sending-domain-status.js.map +1 -0
- package/src/lib/model/template-provenance.d.ts +113 -0
- package/src/lib/model/template-provenance.js +107 -0
- package/src/lib/model/template-provenance.js.map +1 -0
- package/src/lib/model/template-report.d.ts +158 -0
- package/src/lib/model/template-report.js +249 -0
- package/src/lib/model/template-report.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +163 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server-console.d.ts +116 -0
- package/src/lib/server-console.js +422 -0
- package/src/lib/server-console.js.map +1 -0
- package/src/lib/server-email-drafts.d.ts +104 -0
- package/src/lib/server-email-drafts.js +381 -0
- package/src/lib/server-email-drafts.js.map +1 -0
- package/src/lib/server-list-gate.d.ts +183 -0
- package/src/lib/server-list-gate.js +365 -0
- package/src/lib/server-list-gate.js.map +1 -0
- package/src/lib/server-list-import.d.ts +199 -0
- package/src/lib/server-list-import.js +632 -0
- package/src/lib/server-list-import.js.map +1 -0
- package/src/lib/server-suppressions.d.ts +135 -0
- package/src/lib/server-suppressions.js +295 -0
- package/src/lib/server-suppressions.js.map +1 -0
- package/src/lib/server.d.ts +19 -0
- package/src/lib/server.js +834 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +26 -0
- package/src/lib/site.js +81 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/unsubscribe-link.d.ts +311 -0
- package/src/lib/unsubscribe-link.js +398 -0
- package/src/lib/unsubscribe-link.js.map +1 -0
- package/src/lib/utils/create-email-screen.d.ts +59 -0
- package/src/lib/utils/create-email-screen.js +59 -0
- package/src/lib/utils/create-email-screen.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- package/src/lib/utils/generate-preset-id.js.map +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
/**
|
|
20
|
+
* ONE AUDIENCE: who it is, how it is built, and every person on it.
|
|
21
|
+
*
|
|
22
|
+
* ## Why a route rather than an expanded row
|
|
23
|
+
*
|
|
24
|
+
* The membership used to unfold inside the list table, which cost three
|
|
25
|
+
* things a merchant actually uses. A list was not linkable, so "check the
|
|
26
|
+
* Wholesale audience" could not be sent to anybody. The back button walked out
|
|
27
|
+
* of the whole surface rather than out of the list. And the list's own
|
|
28
|
+
* settings had to live under a table they were not about.
|
|
29
|
+
*
|
|
30
|
+
* ## What it reads
|
|
31
|
+
*
|
|
32
|
+
* The list document — one document, whatever the audience's size — plus one
|
|
33
|
+
* server aggregate for the subscriber total, plus the paged membership window
|
|
34
|
+
* the table below draws. The membership is PII and there is one document per
|
|
35
|
+
* subscriber, so it is read HERE, where somebody asked for it, and nowhere
|
|
36
|
+
* that merely lists audiences.
|
|
37
|
+
*
|
|
38
|
+
* ## The rule is stated, not just flagged
|
|
39
|
+
*
|
|
40
|
+
* A dynamic list is a claim about who is on it, and "Rule" as a chip is not
|
|
41
|
+
* that claim. The summary spells out every dimension the rule actually
|
|
42
|
+
* carries, so the answer to "why is this person not in the audience" is on the
|
|
43
|
+
* screen the question gets asked on.
|
|
44
|
+
*/ import { normalizeDynamicListRule, PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
|
|
45
|
+
import { mdiPencilOutline, mdiTrayArrowUp } from "@aglyn/shared-data-mdi";
|
|
46
|
+
import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
47
|
+
import { Button, Chip, Stack, Typography } from "@mui/material";
|
|
48
|
+
import { collection, doc, getCountFromServer } from "firebase/firestore";
|
|
49
|
+
import { useCallback, useEffect, useState } from "react";
|
|
50
|
+
import { useFirestore, useFirestoreDoc, useOrgDataScope } from "@aglyn/tenant-feature-instance";
|
|
51
|
+
import { describeDynamicListRule } from "./dynamic-list-rule-fields.js";
|
|
52
|
+
import ListImportDrawer from "./list-import-drawer.js";
|
|
53
|
+
import ListMembersPanel from "./list-members-panel.js";
|
|
54
|
+
export function ListDetailCard(props) {
|
|
55
|
+
var _list_name, _list_name1;
|
|
56
|
+
var _list_lastEvaluatedAt;
|
|
57
|
+
const { hostId, consentGroup, listId, basePath } = props;
|
|
58
|
+
const firestore = useFirestore();
|
|
59
|
+
const { scope } = useOrgDataScope({
|
|
60
|
+
hostId
|
|
61
|
+
});
|
|
62
|
+
const { data: list, status } = useFirestoreDoc(()=>scope ? doc(firestore, scope[0], scope[1], 'lists', listId) : null, [
|
|
63
|
+
firestore,
|
|
64
|
+
scope,
|
|
65
|
+
listId
|
|
66
|
+
]);
|
|
67
|
+
/*
|
|
68
|
+
* The subscriber total, as a server aggregate.
|
|
69
|
+
*
|
|
70
|
+
* Not the length of the page below: a count beside a paged table is exactly
|
|
71
|
+
* where a page length gets mistaken for a total. Taken once, and re-taken
|
|
72
|
+
* only when a membership is added or removed — a figure that disagreed with
|
|
73
|
+
* the table directly under it would read as the add having failed.
|
|
74
|
+
*/ const [count, setCount] = useState(null);
|
|
75
|
+
const [membershipVersion, setMembershipVersion] = useState(0);
|
|
76
|
+
useEffect(()=>{
|
|
77
|
+
if (!scope) return undefined;
|
|
78
|
+
let active = true;
|
|
79
|
+
void getCountFromServer(collection(firestore, scope[0], scope[1], 'lists', listId, 'members')).then((snapshot)=>{
|
|
80
|
+
if (active) setCount(snapshot.data().count);
|
|
81
|
+
}).catch(()=>{
|
|
82
|
+
if (active) setCount(null);
|
|
83
|
+
});
|
|
84
|
+
return ()=>{
|
|
85
|
+
active = false;
|
|
86
|
+
};
|
|
87
|
+
}, [
|
|
88
|
+
firestore,
|
|
89
|
+
scope,
|
|
90
|
+
listId,
|
|
91
|
+
membershipVersion
|
|
92
|
+
]);
|
|
93
|
+
const onMembershipChanged = useCallback(()=>setMembershipVersion((version)=>version + 1), []);
|
|
94
|
+
/*
|
|
95
|
+
* Importing is a DRAWER opened from the header, not a control inside the
|
|
96
|
+
* membership panel. It is a multi-step act — choose a file, read what is in
|
|
97
|
+
* it, state that you have permission — and the middle step is the one that
|
|
98
|
+
* must not be cramped, because it carries the screening warnings and the
|
|
99
|
+
* consent readout the attestation is given against.
|
|
100
|
+
*/ const [importing, setImporting] = useState(false);
|
|
101
|
+
const audiencesHref = `${basePath}/audiences`;
|
|
102
|
+
const headerActions = /*#__PURE__*/ _jsxs(Stack, {
|
|
103
|
+
direction: "row",
|
|
104
|
+
spacing: 1,
|
|
105
|
+
children: [
|
|
106
|
+
/*#__PURE__*/ _jsx(Button, _extends({
|
|
107
|
+
component: AppLink
|
|
108
|
+
}, {
|
|
109
|
+
componentVariant: 'naked',
|
|
110
|
+
nativeButton: false
|
|
111
|
+
}, {
|
|
112
|
+
href: audiencesHref,
|
|
113
|
+
size: "small",
|
|
114
|
+
color: "primary",
|
|
115
|
+
children: 'All audiences'
|
|
116
|
+
})),
|
|
117
|
+
/*#__PURE__*/ _jsx(Button, _extends({
|
|
118
|
+
component: AppLink
|
|
119
|
+
}, {
|
|
120
|
+
componentVariant: 'naked',
|
|
121
|
+
nativeButton: false
|
|
122
|
+
}, {
|
|
123
|
+
href: `${audiencesHref}/${listId}/edit`,
|
|
124
|
+
size: "small",
|
|
125
|
+
color: "primary",
|
|
126
|
+
variant: "outlined",
|
|
127
|
+
startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
128
|
+
path: mdiPencilOutline.path,
|
|
129
|
+
size: 0.8
|
|
130
|
+
}),
|
|
131
|
+
children: 'Edit list'
|
|
132
|
+
})),
|
|
133
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
134
|
+
size: "small",
|
|
135
|
+
color: "primary",
|
|
136
|
+
variant: "contained",
|
|
137
|
+
startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
138
|
+
path: mdiTrayArrowUp.path,
|
|
139
|
+
size: 0.8
|
|
140
|
+
}),
|
|
141
|
+
onClick: ()=>setImporting(true),
|
|
142
|
+
children: 'Import'
|
|
143
|
+
})
|
|
144
|
+
]
|
|
145
|
+
});
|
|
146
|
+
if (!list) {
|
|
147
|
+
/*
|
|
148
|
+
* Loading and MISSING are different answers, and the difference matters
|
|
149
|
+
* more here than almost anywhere. Not "no members" — a list that cannot be
|
|
150
|
+
* read is a different situation from an empty one, and rendering an empty
|
|
151
|
+
* membership for the first is how somebody comes to believe an audience
|
|
152
|
+
* they built has lost everybody. Reading `status` rather than the absence
|
|
153
|
+
* of data is also what stops the refusal being flashed on every arrival,
|
|
154
|
+
* before the org scope has even resolved.
|
|
155
|
+
*/ const settled = Boolean(scope) && status !== 'loading';
|
|
156
|
+
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
157
|
+
header: 'Audience',
|
|
158
|
+
help: pluginDocsHelp('emailCampaigns', {
|
|
159
|
+
anchor: '#email-lists'
|
|
160
|
+
}),
|
|
161
|
+
contentGutterX: true,
|
|
162
|
+
contentGutterY: true,
|
|
163
|
+
HeaderProps: {
|
|
164
|
+
action: headerActions
|
|
165
|
+
},
|
|
166
|
+
children: /*#__PURE__*/ _jsx(Typography, {
|
|
167
|
+
variant: "body2",
|
|
168
|
+
color: "text.secondary",
|
|
169
|
+
children: settled ? 'This list could not be loaded. It may have been deleted.' : 'Loading this audience…'
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const dynamic = list['kind'] === 'dynamic';
|
|
174
|
+
/*
|
|
175
|
+
* The filters, whichever kind of list this is.
|
|
176
|
+
*
|
|
177
|
+
* On a live list they are what the sweep evaluates. On a fixed one they are
|
|
178
|
+
* a saved SEARCH — the same filters, used to find people to add rather than
|
|
179
|
+
* to decide membership — so they are read and described either way, and only
|
|
180
|
+
* the sentence about what happens next differs.
|
|
181
|
+
*/ const rule = normalizeDynamicListRule(list['rule']);
|
|
182
|
+
const hasFilters = rule.sources.length > 0;
|
|
183
|
+
const summary = hasFilters ? describeDynamicListRule(rule) : [];
|
|
184
|
+
/* The card, named so the page chrome above it is a plain list of
|
|
185
|
+
what this surface publishes upward. */ const card = /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
186
|
+
header: 'Audience',
|
|
187
|
+
help: pluginDocsHelp('emailCampaigns', {
|
|
188
|
+
anchor: '#list-members'
|
|
189
|
+
}),
|
|
190
|
+
contentGutterX: true,
|
|
191
|
+
contentGutterY: true,
|
|
192
|
+
contentBordered: "all",
|
|
193
|
+
HeaderProps: {
|
|
194
|
+
action: headerActions
|
|
195
|
+
},
|
|
196
|
+
children: [
|
|
197
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
198
|
+
spacing: 2,
|
|
199
|
+
children: [
|
|
200
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
201
|
+
direction: "row",
|
|
202
|
+
spacing: 1,
|
|
203
|
+
useFlexGap: true,
|
|
204
|
+
sx: {
|
|
205
|
+
flexWrap: 'wrap',
|
|
206
|
+
alignItems: 'center'
|
|
207
|
+
},
|
|
208
|
+
children: [
|
|
209
|
+
dynamic ? /*#__PURE__*/ _jsx(Chip, {
|
|
210
|
+
size: "small",
|
|
211
|
+
color: "primary",
|
|
212
|
+
variant: "outlined",
|
|
213
|
+
label: ((_list_lastEvaluatedAt = list['lastEvaluatedAt']) == null ? void 0 : _list_lastEvaluatedAt.toDate) ? `Rule · ${list['lastEvaluatedAt'].toDate().toLocaleString()}` : 'Rule · not yet evaluated'
|
|
214
|
+
}) : /*#__PURE__*/ _jsx(Chip, {
|
|
215
|
+
size: "small",
|
|
216
|
+
variant: "outlined",
|
|
217
|
+
label: "Fixed membership"
|
|
218
|
+
}),
|
|
219
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
220
|
+
variant: "body2",
|
|
221
|
+
color: "text.secondary",
|
|
222
|
+
children: count === null ? 'Counting subscribers…' : count === 1 ? '1 subscriber' : `${count.toLocaleString()} subscribers`
|
|
223
|
+
})
|
|
224
|
+
]
|
|
225
|
+
}),
|
|
226
|
+
dynamic && hasFilters ? /*#__PURE__*/ _jsx(Stack, {
|
|
227
|
+
spacing: 0.5,
|
|
228
|
+
children: summary.map((clause)=>/*#__PURE__*/ _jsx(Typography, {
|
|
229
|
+
variant: "body2",
|
|
230
|
+
color: "text.secondary",
|
|
231
|
+
children: clause
|
|
232
|
+
}, clause))
|
|
233
|
+
}) : null,
|
|
234
|
+
scope ? /*#__PURE__*/ _jsx(ListMembersPanel, {
|
|
235
|
+
hostId: hostId,
|
|
236
|
+
consentGroup: consentGroup,
|
|
237
|
+
scope: scope,
|
|
238
|
+
listId: listId,
|
|
239
|
+
listName: String((_list_name = list['name']) != null ? _list_name : ''),
|
|
240
|
+
onMembershipChanged: onMembershipChanged,
|
|
241
|
+
/*
|
|
242
|
+
Offered only on a FIXED list. A live list's membership is the
|
|
243
|
+
sweep's to decide, and hand-adding its own matches would write
|
|
244
|
+
`via: 'manual'` copies of rows the sweep already owns — rows it
|
|
245
|
+
then may not reconcile away when the person stops matching.
|
|
246
|
+
*/ findRule: dynamic || !hasFilters ? null : rule,
|
|
247
|
+
ruleSummary: summary
|
|
248
|
+
}) : null
|
|
249
|
+
]
|
|
250
|
+
}),
|
|
251
|
+
importing ? /*#__PURE__*/ _jsx(ListImportDrawer, {
|
|
252
|
+
open: true,
|
|
253
|
+
onClose: ()=>setImporting(false),
|
|
254
|
+
hostId: hostId,
|
|
255
|
+
listId: listId,
|
|
256
|
+
listName: String((_list_name1 = list['name']) != null ? _list_name1 : ''),
|
|
257
|
+
onMembershipChanged: onMembershipChanged
|
|
258
|
+
}) : null
|
|
259
|
+
]
|
|
260
|
+
});
|
|
261
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
262
|
+
children: [
|
|
263
|
+
/*#__PURE__*/ _jsx(PageHeaderRecord, {
|
|
264
|
+
title: String(list['name'] || listId)
|
|
265
|
+
}),
|
|
266
|
+
card
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
ListDetailCard.displayName = 'ListDetailCard';
|
|
271
|
+
export default ListDetailCard;
|
|
272
|
+
|
|
273
|
+
//# sourceMappingURL=list-detail-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/list-detail-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * ONE AUDIENCE: who it is, how it is built, and every person on it.\n *\n * ## Why a route rather than an expanded row\n *\n * The membership used to unfold inside the list table, which cost three\n * things a merchant actually uses. A list was not linkable, so \"check the\n * Wholesale audience\" could not be sent to anybody. The back button walked out\n * of the whole surface rather than out of the list. And the list's own\n * settings had to live under a table they were not about.\n *\n * ## What it reads\n *\n * The list document — one document, whatever the audience's size — plus one\n * server aggregate for the subscriber total, plus the paged membership window\n * the table below draws. The membership is PII and there is one document per\n * subscriber, so it is read HERE, where somebody asked for it, and nowhere\n * that merely lists audiences.\n *\n * ## The rule is stated, not just flagged\n *\n * A dynamic list is a claim about who is on it, and \"Rule\" as a chip is not\n * that claim. The summary spells out every dimension the rule actually\n * carries, so the answer to \"why is this person not in the audience\" is on the\n * screen the question gets asked on.\n */\n\nimport {\n normalizeDynamicListRule,\n PageHeaderRecord,\n pluginDocsHelp,\n type ConsentGroup,\n} from '@aglyn/aglyn'\nimport { mdiPencilOutline, mdiTrayArrowUp } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport { collection, doc, getCountFromServer } from 'firebase/firestore'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useFirestore, useFirestoreDoc, useOrgDataScope } from '@aglyn/tenant-feature-instance'\nimport { describeDynamicListRule } from './dynamic-list-rule-fields'\nimport ListImportDrawer from './list-import-drawer'\nimport ListMembersPanel from './list-members-panel'\n\nexport interface ListDetailCardProps {\n hostId: string\n /**\n * The controller this audience is being read AS, resolved by the page from\n * the org document it already holds.\n *\n * Passed through rather than resolved here: the group is a fact about the\n * ORG, one lookup serves every section, and a card that resolved its own\n * would be a second place for the answer to come from.\n */\n consentGroup: ConsentGroup\n listId: string\n /** The emails hub URL, for the way back to the audiences list. */\n basePath: string\n}\n\nexport function ListDetailCard(props: ListDetailCardProps) {\n const { hostId, consentGroup, listId, basePath } = props\n const firestore = useFirestore()\n const { scope } = useOrgDataScope({ hostId })\n\n const { data: list, status } = useFirestoreDoc<Record<string, any>>(\n () =>\n scope ? doc(firestore, scope[0], scope[1], 'lists', listId) : null,\n [firestore, scope, listId],\n )\n\n /*\n * The subscriber total, as a server aggregate.\n *\n * Not the length of the page below: a count beside a paged table is exactly\n * where a page length gets mistaken for a total. Taken once, and re-taken\n * only when a membership is added or removed — a figure that disagreed with\n * the table directly under it would read as the add having failed.\n */\n const [count, setCount] = useState<number | null>(null)\n const [membershipVersion, setMembershipVersion] = useState(0)\n useEffect(() => {\n if (!scope) return undefined\n let active = true\n void getCountFromServer(\n collection(firestore, scope[0], scope[1], 'lists', listId, 'members'),\n )\n .then((snapshot) => {\n if (active) setCount(snapshot.data().count)\n })\n .catch(() => {\n if (active) setCount(null)\n })\n return () => {\n active = false\n }\n }, [firestore, scope, listId, membershipVersion])\n\n const onMembershipChanged = useCallback(\n () => setMembershipVersion((version) => version + 1),\n [],\n )\n\n /*\n * Importing is a DRAWER opened from the header, not a control inside the\n * membership panel. It is a multi-step act — choose a file, read what is in\n * it, state that you have permission — and the middle step is the one that\n * must not be cramped, because it carries the screening warnings and the\n * consent readout the attestation is given against.\n */\n const [importing, setImporting] = useState(false)\n\n const audiencesHref = `${basePath}/audiences`\n const headerActions = (\n <Stack direction=\"row\" spacing={1}>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={audiencesHref}\n size=\"small\"\n color=\"primary\"\n >\n {'All audiences'}\n </Button>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={`${audiencesHref}/${listId}/edit`}\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n >\n {'Edit list'}\n </Button>\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiTrayArrowUp.path} size={0.8} />}\n onClick={() => setImporting(true)}\n >\n {'Import'}\n </Button>\n </Stack>\n )\n\n if (!list) {\n /*\n * Loading and MISSING are different answers, and the difference matters\n * more here than almost anywhere. Not \"no members\" — a list that cannot be\n * read is a different situation from an empty one, and rendering an empty\n * membership for the first is how somebody comes to believe an audience\n * they built has lost everybody. Reading `status` rather than the absence\n * of data is also what stops the refusal being flashed on every arrival,\n * before the org scope has even resolved.\n */\n const settled = Boolean(scope) && status !== 'loading'\n return (\n <CardDisplay\n header={'Audience'}\n help={pluginDocsHelp('emailCampaigns', { anchor: '#email-lists' })}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'This list could not be loaded. It may have been deleted.'\n : 'Loading this audience…'}\n </Typography>\n </CardDisplay>\n )\n }\n\n const dynamic = list['kind'] === 'dynamic'\n /*\n * The filters, whichever kind of list this is.\n *\n * On a live list they are what the sweep evaluates. On a fixed one they are\n * a saved SEARCH — the same filters, used to find people to add rather than\n * to decide membership — so they are read and described either way, and only\n * the sentence about what happens next differs.\n */\n const rule = normalizeDynamicListRule(list['rule'])\n const hasFilters = rule.sources.length > 0\n const summary = hasFilters ? describeDynamicListRule(rule) : []\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Audience'}\n help={pluginDocsHelp('emailCampaigns', { anchor: '#list-members' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n {dynamic ? (\n <Chip\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n label={\n list['lastEvaluatedAt']?.toDate\n ? `Rule · ${list['lastEvaluatedAt']\n .toDate()\n .toLocaleString()}`\n : 'Rule · not yet evaluated'\n }\n />\n ) : (\n <Chip size=\"small\" variant=\"outlined\" label=\"Fixed membership\" />\n )}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {count === null\n ? 'Counting subscribers…'\n : count === 1\n ? '1 subscriber'\n : `${count.toLocaleString()} subscribers`}\n </Typography>\n </Stack>\n\n {/*\n A live list's filters are stated HERE rather than only on the edit\n page: they are the answer to \"why is this person not in the\n audience\", which is a question asked while looking at the membership.\n A fixed list states them inside the find-people control instead,\n beside the button that acts on them.\n */}\n {dynamic && hasFilters ? (\n <Stack spacing={0.5}>\n {summary.map((clause) => (\n <Typography key={clause} variant=\"body2\" color=\"text.secondary\">\n {clause}\n </Typography>\n ))}\n </Stack>\n ) : null}\n\n {scope ? (\n <ListMembersPanel\n hostId={hostId}\n consentGroup={consentGroup}\n scope={scope as readonly [string, string]}\n listId={listId}\n listName={String(list['name'] ?? '')}\n onMembershipChanged={onMembershipChanged}\n /*\n Offered only on a FIXED list. A live list's membership is the\n sweep's to decide, and hand-adding its own matches would write\n `via: 'manual'` copies of rows the sweep already owns — rows it\n then may not reconcile away when the person stops matching.\n */\n findRule={dynamic || !hasFilters ? null : rule}\n ruleSummary={summary}\n />\n ) : null}\n </Stack>\n {/*\n Mounted only while it is open, so a page nobody is importing on runs\n none of its effects — the drawer looks for an unfinished import when\n it opens, and that read must not be a cost of visiting an audience.\n */}\n {importing ? (\n <ListImportDrawer\n open\n onClose={() => setImporting(false)}\n hostId={hostId}\n listId={listId}\n listName={String(list['name'] ?? '')}\n onMembershipChanged={onMembershipChanged}\n />\n ) : null}\n </CardDisplay>\n )\n\n return (\n <>\n {/* The page heading and the trail name the audience; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord title={String(list['name'] || listId)} />\n {card}\n </>\n )\n}\nListDetailCard.displayName = 'ListDetailCard'\n\nexport default ListDetailCard\n"],"names":["normalizeDynamicListRule","PageHeaderRecord","pluginDocsHelp","mdiPencilOutline","mdiTrayArrowUp","AppLink","CardDisplay","MdiIcon","Button","Chip","Stack","Typography","collection","doc","getCountFromServer","useCallback","useEffect","useState","useFirestore","useFirestoreDoc","useOrgDataScope","describeDynamicListRule","ListImportDrawer","ListMembersPanel","ListDetailCard","props","list","hostId","consentGroup","listId","basePath","firestore","scope","data","status","count","setCount","membershipVersion","setMembershipVersion","undefined","active","then","snapshot","catch","onMembershipChanged","version","importing","setImporting","audiencesHref","headerActions","direction","spacing","component","componentVariant","nativeButton","href","size","color","variant","startIcon","path","onClick","settled","Boolean","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","dynamic","rule","hasFilters","sources","length","summary","card","contentBordered","useFlexGap","sx","flexWrap","alignItems","label","toDate","toLocaleString","map","clause","listName","String","findRule","ruleSummary","open","onClose","title","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ListEditCardProps {
|
|
2
|
+
hostId: string;
|
|
3
|
+
listId: string;
|
|
4
|
+
/** The emails hub URL, which the audiences routes hang beneath. */
|
|
5
|
+
basePath: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function ListEditCard(props: ListEditCardProps): import("react").JSX.Element;
|
|
8
|
+
export declare namespace ListEditCard {
|
|
9
|
+
var displayName: string;
|
|
10
|
+
}
|
|
11
|
+
export default ListEditCard;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
/**
|
|
20
|
+
* A LIST'S SETTINGS: its name, how its membership is decided, and the filters
|
|
21
|
+
* that decide it.
|
|
22
|
+
*
|
|
23
|
+
* ## Why it is its own route
|
|
24
|
+
*
|
|
25
|
+
* Editing the rule that decides an audience is not a thing to do in a row of a
|
|
26
|
+
* table. It is nine fields, several of them consequential enough that the
|
|
27
|
+
* merchant should see what they currently say before changing them, and a
|
|
28
|
+
* rule saved by accident silently re-populates the audience on the next sweep.
|
|
29
|
+
* A route also means the edit is linkable and the back button leaves it.
|
|
30
|
+
*
|
|
31
|
+
* ## The rule is edited as a DRAFT and written once
|
|
32
|
+
*
|
|
33
|
+
* The controls hold text; `draftToRule` turns it into a rule; Save writes that
|
|
34
|
+
* rule. Nothing is written per keystroke, because a dynamic list's rule is
|
|
35
|
+
* read by a sweep every fifteen minutes, which would otherwise re-materialize
|
|
36
|
+
* the audience against a half-typed intermediate state.
|
|
37
|
+
*
|
|
38
|
+
* ## The two outcomes, and the sentence that tells them apart
|
|
39
|
+
*
|
|
40
|
+
* The SAME filters serve both kinds of list, and the difference is what
|
|
41
|
+
* happens afterwards:
|
|
42
|
+
*
|
|
43
|
+
* - **Live** keeps the rule. Membership is re-evaluated on the sweep and the
|
|
44
|
+
* audience keeps growing as people start matching.
|
|
45
|
+
* - **Fixed** does not. The filters are a way to FIND people; who is on the
|
|
46
|
+
* list is whoever was added, and it stays that way.
|
|
47
|
+
*
|
|
48
|
+
* A reader must never be unsure which they have, so the control says it in
|
|
49
|
+
* those words and the page repeats it under whichever is selected. Switching
|
|
50
|
+
* is safe in both directions and neither drops anybody: the sweep reconciles
|
|
51
|
+
* away only rows IT wrote (`via: 'rule'`), so a person somebody added by hand
|
|
52
|
+
* survives becoming live, and everyone on the list survives becoming fixed.
|
|
53
|
+
* The rule is kept on a fixed list rather than deleted — it is the filter that
|
|
54
|
+
* found these people, and losing it on a toggle would lose the work.
|
|
55
|
+
*/ import { normalizeDynamicListRule, PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
|
|
56
|
+
import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
57
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
58
|
+
import { Alert, Button, MenuItem, Stack, TextField, Typography } from "@mui/material";
|
|
59
|
+
import { doc, updateDoc } from "firebase/firestore";
|
|
60
|
+
import { useRouter } from "next/navigation";
|
|
61
|
+
import { useEffect, useRef, useState } from "react";
|
|
62
|
+
import { useFirestore, useFirestoreDoc, useOrgDataScope } from "@aglyn/tenant-feature-instance";
|
|
63
|
+
import DynamicListRuleFields, { draftToRule, useRuleDraft } from "./dynamic-list-rule-fields.js";
|
|
64
|
+
export function ListEditCard(props) {
|
|
65
|
+
var _ref;
|
|
66
|
+
const { hostId, listId, basePath } = props;
|
|
67
|
+
const firestore = useFirestore();
|
|
68
|
+
const router = useRouter();
|
|
69
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
70
|
+
const { scope } = useOrgDataScope({
|
|
71
|
+
hostId
|
|
72
|
+
});
|
|
73
|
+
const { data: list, status } = useFirestoreDoc(()=>scope ? doc(firestore, scope[0], scope[1], 'lists', listId) : null, [
|
|
74
|
+
firestore,
|
|
75
|
+
scope,
|
|
76
|
+
listId
|
|
77
|
+
]);
|
|
78
|
+
const storedName = String((_ref = list == null ? void 0 : list['name']) != null ? _ref : '');
|
|
79
|
+
const storedKind = (list == null ? void 0 : list['kind']) === 'dynamic' ? 'dynamic' : 'manual';
|
|
80
|
+
/*
|
|
81
|
+
* Normalized before the form ever sees it, by the same function the
|
|
82
|
+
* materializer reads it back through. A rule shown one way and evaluated
|
|
83
|
+
* another is the defect this screen exists to make visible.
|
|
84
|
+
*/ const storedRule = list ? normalizeDynamicListRule(list['rule']) : undefined;
|
|
85
|
+
const [name, setName] = useState('');
|
|
86
|
+
const [kind, setKind] = useState('manual');
|
|
87
|
+
const dynamic = kind === 'dynamic';
|
|
88
|
+
/*
|
|
89
|
+
* Seeded ONCE, when the document first arrives. The listen is live, so
|
|
90
|
+
* re-seeding on every snapshot would take the field away from whoever is
|
|
91
|
+
* typing in it — including on the snapshot their own save produces.
|
|
92
|
+
*/ const seeded = useRef(false);
|
|
93
|
+
useEffect(()=>{
|
|
94
|
+
if (!list || seeded.current) return;
|
|
95
|
+
seeded.current = true;
|
|
96
|
+
setName(storedName);
|
|
97
|
+
setKind(storedKind);
|
|
98
|
+
}, [
|
|
99
|
+
list,
|
|
100
|
+
storedName,
|
|
101
|
+
storedKind
|
|
102
|
+
]);
|
|
103
|
+
const [draft, setDraft] = useRuleDraft(storedRule, listId);
|
|
104
|
+
const [busy, setBusy] = useState(false);
|
|
105
|
+
const audiencesHref = `${basePath}/audiences`;
|
|
106
|
+
const detailHref = `${audiencesHref}/${listId}`;
|
|
107
|
+
const handleSave = async ()=>{
|
|
108
|
+
const next = name.trim();
|
|
109
|
+
/*
|
|
110
|
+
* An empty name is not a rename. The list table orders on `name`, and
|
|
111
|
+
* `orderBy` drops a document that does not carry the field — so a list
|
|
112
|
+
* renamed to nothing would vanish from the page that lists it.
|
|
113
|
+
*/ if (!next || !scope || busy) return;
|
|
114
|
+
setBusy(true);
|
|
115
|
+
try {
|
|
116
|
+
await updateDoc(doc(firestore, scope[0], scope[1], 'lists', listId), {
|
|
117
|
+
name: next,
|
|
118
|
+
kind,
|
|
119
|
+
/*
|
|
120
|
+
* The rule is written whichever kind was chosen. On a live list it is
|
|
121
|
+
* what the sweep evaluates; on a fixed one it is the filter that finds
|
|
122
|
+
* people to add, and keeping it is what lets a merchant refine the
|
|
123
|
+
* search across visits instead of retyping it.
|
|
124
|
+
*/ rule: draftToRule(draft),
|
|
125
|
+
/*
|
|
126
|
+
* WHICH SITE'S people the rule draws from.
|
|
127
|
+
*
|
|
128
|
+
* Lists are org-shared but leads, members and form submissions are
|
|
129
|
+
* host-owned, and org contacts are read narrowed to one host. A rule
|
|
130
|
+
* with no host has no silos at all, so the sweep skips it.
|
|
131
|
+
*/ hostId
|
|
132
|
+
});
|
|
133
|
+
enqueueSnackbar('List saved', {
|
|
134
|
+
variant: 'success',
|
|
135
|
+
persist: false
|
|
136
|
+
});
|
|
137
|
+
router.push(detailHref);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error(error);
|
|
140
|
+
enqueueSnackbar('The list was not saved', {
|
|
141
|
+
variant: 'error'
|
|
142
|
+
});
|
|
143
|
+
} finally{
|
|
144
|
+
setBusy(false);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const headerActions = /*#__PURE__*/ _jsx(Button, _extends({
|
|
148
|
+
component: AppLink
|
|
149
|
+
}, {
|
|
150
|
+
componentVariant: 'naked',
|
|
151
|
+
nativeButton: false
|
|
152
|
+
}, {
|
|
153
|
+
href: detailHref,
|
|
154
|
+
size: "small",
|
|
155
|
+
color: "primary",
|
|
156
|
+
children: 'Back to the audience'
|
|
157
|
+
}));
|
|
158
|
+
if (!list) {
|
|
159
|
+
const settled = Boolean(scope) && status !== 'loading';
|
|
160
|
+
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
161
|
+
header: 'Edit list',
|
|
162
|
+
help: pluginDocsHelp('emailCampaigns', {
|
|
163
|
+
anchor: '#email-lists'
|
|
164
|
+
}),
|
|
165
|
+
contentGutterX: true,
|
|
166
|
+
contentGutterY: true,
|
|
167
|
+
HeaderProps: {
|
|
168
|
+
action: headerActions
|
|
169
|
+
},
|
|
170
|
+
children: /*#__PURE__*/ _jsx(Typography, {
|
|
171
|
+
variant: "body2",
|
|
172
|
+
color: "text.secondary",
|
|
173
|
+
children: settled ? 'This list could not be loaded. It may have been deleted.' : 'Loading this audience…'
|
|
174
|
+
})
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/* The card, named so the page chrome above it is a plain list of
|
|
178
|
+
what this surface publishes upward. */ const card = /*#__PURE__*/ _jsx(CardDisplay, {
|
|
179
|
+
header: 'Edit audience',
|
|
180
|
+
help: pluginDocsHelp('emailCampaigns', {
|
|
181
|
+
anchor: dynamic ? '#lists-built-from-a-rule' : '#manual-lists'
|
|
182
|
+
}),
|
|
183
|
+
contentGutterX: true,
|
|
184
|
+
contentGutterY: true,
|
|
185
|
+
contentBordered: "all",
|
|
186
|
+
HeaderProps: {
|
|
187
|
+
action: headerActions
|
|
188
|
+
},
|
|
189
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
190
|
+
spacing: 2,
|
|
191
|
+
children: [
|
|
192
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
193
|
+
direction: "row",
|
|
194
|
+
spacing: 1,
|
|
195
|
+
useFlexGap: true,
|
|
196
|
+
sx: {
|
|
197
|
+
flexWrap: 'wrap'
|
|
198
|
+
},
|
|
199
|
+
children: [
|
|
200
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
201
|
+
size: "small",
|
|
202
|
+
label: "List name",
|
|
203
|
+
value: name,
|
|
204
|
+
onChange: (event)=>setName(event.target.value),
|
|
205
|
+
sx: {
|
|
206
|
+
flexGrow: 1,
|
|
207
|
+
minWidth: 240,
|
|
208
|
+
maxWidth: 360
|
|
209
|
+
}
|
|
210
|
+
}),
|
|
211
|
+
/*#__PURE__*/ _jsxs(TextField, {
|
|
212
|
+
select: true,
|
|
213
|
+
size: "small",
|
|
214
|
+
label: "Membership",
|
|
215
|
+
value: kind,
|
|
216
|
+
onChange: (event)=>setKind(event.target.value),
|
|
217
|
+
sx: {
|
|
218
|
+
minWidth: 240
|
|
219
|
+
},
|
|
220
|
+
children: [
|
|
221
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
222
|
+
value: "dynamic",
|
|
223
|
+
children: 'Live — keeps growing'
|
|
224
|
+
}),
|
|
225
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
226
|
+
value: "manual",
|
|
227
|
+
children: 'Fixed — does not change'
|
|
228
|
+
})
|
|
229
|
+
]
|
|
230
|
+
})
|
|
231
|
+
]
|
|
232
|
+
}),
|
|
233
|
+
/*#__PURE__*/ _jsx(Alert, {
|
|
234
|
+
severity: dynamic ? 'info' : 'success',
|
|
235
|
+
children: dynamic ? 'Everyone matching these filters is enrolled automatically, ' + 'about every fifteen minutes, and leaves when they stop ' + 'matching. Anyone added by hand stays. Being matched is not ' + 'consent to be emailed — a campaign still only reaches people ' + 'whose consent is on record.' : 'Membership does not change on its own. Use the filters below ' + 'to FIND people, then add them from the audience page — who ' + 'you add is who stays on the list.'
|
|
236
|
+
}),
|
|
237
|
+
scope ? /*#__PURE__*/ _jsx(DynamicListRuleFields, {
|
|
238
|
+
scope: scope,
|
|
239
|
+
hostId: hostId,
|
|
240
|
+
draft: draft,
|
|
241
|
+
onChange: setDraft,
|
|
242
|
+
listId: listId
|
|
243
|
+
}) : null,
|
|
244
|
+
draft.sources.length || !dynamic ? null : /*#__PURE__*/ _jsx(Alert, {
|
|
245
|
+
severity: "warning",
|
|
246
|
+
children: 'These filters draw from no source, so they match nobody. The ' + 'next sweep would empty the list of everyone the rule enrolled.'
|
|
247
|
+
}),
|
|
248
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
249
|
+
direction: "row",
|
|
250
|
+
spacing: 1,
|
|
251
|
+
children: [
|
|
252
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
253
|
+
size: "small",
|
|
254
|
+
variant: "contained",
|
|
255
|
+
color: "primary",
|
|
256
|
+
disabled: busy || !name.trim() || !scope,
|
|
257
|
+
onClick: ()=>void handleSave(),
|
|
258
|
+
children: busy ? 'Saving…' : 'Save'
|
|
259
|
+
}),
|
|
260
|
+
/*#__PURE__*/ _jsx(Button, _extends({
|
|
261
|
+
component: AppLink
|
|
262
|
+
}, {
|
|
263
|
+
componentVariant: 'naked',
|
|
264
|
+
nativeButton: false
|
|
265
|
+
}, {
|
|
266
|
+
href: detailHref,
|
|
267
|
+
size: "small",
|
|
268
|
+
children: 'Cancel'
|
|
269
|
+
}))
|
|
270
|
+
]
|
|
271
|
+
})
|
|
272
|
+
]
|
|
273
|
+
})
|
|
274
|
+
});
|
|
275
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
276
|
+
children: [
|
|
277
|
+
/*#__PURE__*/ _jsx(PageHeaderRecord, {
|
|
278
|
+
title: storedName || listId
|
|
279
|
+
}),
|
|
280
|
+
card
|
|
281
|
+
]
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
ListEditCard.displayName = 'ListEditCard';
|
|
285
|
+
export default ListEditCard;
|
|
286
|
+
|
|
287
|
+
//# sourceMappingURL=list-edit-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/list-edit-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * A LIST'S SETTINGS: its name, how its membership is decided, and the filters\n * that decide it.\n *\n * ## Why it is its own route\n *\n * Editing the rule that decides an audience is not a thing to do in a row of a\n * table. It is nine fields, several of them consequential enough that the\n * merchant should see what they currently say before changing them, and a\n * rule saved by accident silently re-populates the audience on the next sweep.\n * A route also means the edit is linkable and the back button leaves it.\n *\n * ## The rule is edited as a DRAFT and written once\n *\n * The controls hold text; `draftToRule` turns it into a rule; Save writes that\n * rule. Nothing is written per keystroke, because a dynamic list's rule is\n * read by a sweep every fifteen minutes, which would otherwise re-materialize\n * the audience against a half-typed intermediate state.\n *\n * ## The two outcomes, and the sentence that tells them apart\n *\n * The SAME filters serve both kinds of list, and the difference is what\n * happens afterwards:\n *\n * - **Live** keeps the rule. Membership is re-evaluated on the sweep and the\n * audience keeps growing as people start matching.\n * - **Fixed** does not. The filters are a way to FIND people; who is on the\n * list is whoever was added, and it stays that way.\n *\n * A reader must never be unsure which they have, so the control says it in\n * those words and the page repeats it under whichever is selected. Switching\n * is safe in both directions and neither drops anybody: the sweep reconciles\n * away only rows IT wrote (`via: 'rule'`), so a person somebody added by hand\n * survives becoming live, and everyone on the list survives becoming fixed.\n * The rule is kept on a fixed list rather than deleted — it is the filter that\n * found these people, and losing it on a toggle would lose the work.\n */\n\nimport {\n normalizeDynamicListRule,\n PageHeaderRecord,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useRef, useState } from 'react'\nimport {\n useFirestore,\n useFirestoreDoc,\n useOrgDataScope,\n} from '@aglyn/tenant-feature-instance'\nimport DynamicListRuleFields, {\n draftToRule,\n useRuleDraft,\n} from './dynamic-list-rule-fields'\n\nexport interface ListEditCardProps {\n hostId: string\n listId: string\n /** The emails hub URL, which the audiences routes hang beneath. */\n basePath: string\n}\n\nexport function ListEditCard(props: ListEditCardProps) {\n const { hostId, listId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { scope } = useOrgDataScope({ hostId })\n\n const { data: list, status } = useFirestoreDoc<Record<string, any>>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'lists', listId) : null),\n [firestore, scope, listId],\n )\n\n const storedName = String(list?.['name'] ?? '')\n const storedKind: 'manual' | 'dynamic' =\n list?.['kind'] === 'dynamic' ? 'dynamic' : 'manual'\n /*\n * Normalized before the form ever sees it, by the same function the\n * materializer reads it back through. A rule shown one way and evaluated\n * another is the defect this screen exists to make visible.\n */\n const storedRule = list ? normalizeDynamicListRule(list['rule']) : undefined\n\n const [name, setName] = useState('')\n const [kind, setKind] = useState<'manual' | 'dynamic'>('manual')\n const dynamic = kind === 'dynamic'\n /*\n * Seeded ONCE, when the document first arrives. The listen is live, so\n * re-seeding on every snapshot would take the field away from whoever is\n * typing in it — including on the snapshot their own save produces.\n */\n const seeded = useRef(false)\n useEffect(() => {\n if (!list || seeded.current) return\n seeded.current = true\n setName(storedName)\n setKind(storedKind)\n }, [list, storedName, storedKind])\n\n const [draft, setDraft] = useRuleDraft(storedRule, listId)\n const [busy, setBusy] = useState(false)\n\n const audiencesHref = `${basePath}/audiences`\n const detailHref = `${audiencesHref}/${listId}`\n\n const handleSave = async () => {\n const next = name.trim()\n /*\n * An empty name is not a rename. The list table orders on `name`, and\n * `orderBy` drops a document that does not carry the field — so a list\n * renamed to nothing would vanish from the page that lists it.\n */\n if (!next || !scope || busy) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, scope[0], scope[1], 'lists', listId), {\n name: next,\n kind,\n /*\n * The rule is written whichever kind was chosen. On a live list it is\n * what the sweep evaluates; on a fixed one it is the filter that finds\n * people to add, and keeping it is what lets a merchant refine the\n * search across visits instead of retyping it.\n */\n rule: draftToRule(draft),\n /*\n * WHICH SITE'S people the rule draws from.\n *\n * Lists are org-shared but leads, members and form submissions are\n * host-owned, and org contacts are read narrowed to one host. A rule\n * with no host has no silos at all, so the sweep skips it.\n */\n hostId,\n })\n enqueueSnackbar('List saved', { variant: 'success', persist: false })\n router.push(detailHref)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The list was not saved', { variant: 'error' })\n } finally {\n setBusy(false)\n }\n }\n\n const headerActions = (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={detailHref}\n size=\"small\"\n color=\"primary\"\n >\n {'Back to the audience'}\n </Button>\n )\n\n if (!list) {\n const settled = Boolean(scope) && status !== 'loading'\n return (\n <CardDisplay\n header={'Edit list'}\n help={pluginDocsHelp('emailCampaigns', { anchor: '#email-lists' })}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'This list could not be loaded. It may have been deleted.'\n : 'Loading this audience…'}\n </Typography>\n </CardDisplay>\n )\n }\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Edit audience'}\n help={pluginDocsHelp('emailCampaigns', {\n anchor: dynamic ? '#lists-built-from-a-rule' : '#manual-lists',\n })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1} useFlexGap sx={{ flexWrap: 'wrap' }}>\n <TextField\n size=\"small\"\n label=\"List name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n sx={{ flexGrow: 1, minWidth: 240, maxWidth: 360 }}\n />\n {/*\n The one decision on this page that changes what the list DOES, so\n the options are named for their outcome rather than for the field\n they set. \"Dynamic\" and \"manual\" describe the implementation; \"keeps\n growing\" and \"does not change\" describe what the merchant gets.\n */}\n <TextField\n select\n size=\"small\"\n label=\"Membership\"\n value={kind}\n onChange={(event) =>\n setKind(event.target.value as 'manual' | 'dynamic')\n }\n sx={{ minWidth: 240 }}\n >\n <MenuItem value=\"dynamic\">{'Live — keeps growing'}</MenuItem>\n <MenuItem value=\"manual\">{'Fixed — does not change'}</MenuItem>\n </TextField>\n </Stack>\n\n <Alert severity={dynamic ? 'info' : 'success'}>\n {dynamic\n ? 'Everyone matching these filters is enrolled automatically, ' +\n 'about every fifteen minutes, and leaves when they stop ' +\n 'matching. Anyone added by hand stays. Being matched is not ' +\n 'consent to be emailed — a campaign still only reaches people ' +\n 'whose consent is on record.'\n : 'Membership does not change on its own. Use the filters below ' +\n 'to FIND people, then add them from the audience page — who ' +\n 'you add is who stays on the list.'}\n </Alert>\n\n {scope ? (\n <DynamicListRuleFields\n scope={scope as readonly [string, string]}\n hostId={hostId}\n draft={draft}\n onChange={setDraft}\n listId={listId}\n />\n ) : null}\n {/*\n A rule with no source matches nobody, and an audience emptied that\n way looks exactly like one whose sweep has not run yet. It is only a\n warning on a LIVE list: on a fixed one an unset filter selects\n nothing to add and takes nobody off.\n */}\n {draft.sources.length || !dynamic ? null : (\n <Alert severity=\"warning\">\n {'These filters draw from no source, so they match nobody. The ' +\n 'next sweep would empty the list of everyone the rule enrolled.'}\n </Alert>\n )}\n\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={busy || !name.trim() || !scope}\n onClick={() => void handleSave()}\n >\n {busy ? 'Saving…' : 'Save'}\n </Button>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={detailHref}\n size=\"small\"\n >\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n )\n\n return (\n <>\n {/*\n The audience being edited. This route is one level BELOW it, and the\n way back is the card header's own button — the trail names where the\n reader is and does not link its last step.\n */}\n <PageHeaderRecord title={storedName || listId} />\n {card}\n </>\n )\n}\nListEditCard.displayName = 'ListEditCard'\n\nexport default ListEditCard\n"],"names":["normalizeDynamicListRule","PageHeaderRecord","pluginDocsHelp","AppLink","CardDisplay","useSnackbar","Alert","Button","MenuItem","Stack","TextField","Typography","doc","updateDoc","useRouter","useEffect","useRef","useState","useFirestore","useFirestoreDoc","useOrgDataScope","DynamicListRuleFields","draftToRule","useRuleDraft","ListEditCard","props","hostId","listId","basePath","firestore","router","enqueueSnackbar","scope","data","list","status","storedName","String","storedKind","storedRule","undefined","name","setName","kind","setKind","dynamic","seeded","current","draft","setDraft","busy","setBusy","audiencesHref","detailHref","handleSave","next","trim","rule","variant","persist","push","error","console","headerActions","component","componentVariant","nativeButton","href","size","color","settled","Boolean","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","card","contentBordered","spacing","direction","useFlexGap","sx","flexWrap","label","value","onChange","event","target","flexGrow","minWidth","maxWidth","select","severity","sources","length","disabled","onClick","title","displayName"],"mappings":"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"}
|