@aglyn/plugins-email 1.0.0-beta.232 → 1.0.0-beta.234
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/package.json +16 -16
- package/src/lib/components/email-screens-card.js +33 -6
- package/src/lib/components/email-screens-card.js.map +1 -1
- package/src/lib/components/list-members-panel.js +38 -7
- package/src/lib/components/list-members-panel.js.map +1 -1
- package/src/lib/components/lists-card.js +35 -7
- package/src/lib/components/lists-card.js.map +1 -1
- package/src/lib/components/org-email-templates-card.js +34 -7
- package/src/lib/components/org-email-templates-card.js.map +1 -1
- package/src/lib/components/suppressions-card.js +24 -6
- package/src/lib/components/suppressions-card.js.map +1 -1
- package/src/lib/constants/list-queries.js +73 -9
- package/src/lib/constants/list-queries.js.map +1 -1
- package/src/lib/model/email-design-document.d.ts +1 -1
- package/src/lib/model/email-design-document.js +1 -1
- package/src/lib/model/email-design-document.js.map +1 -1
|
@@ -21,7 +21,28 @@
|
|
|
21
21
|
* page of the answer. A question no query can answer is not offered. Each
|
|
22
22
|
* list's spec holds `cloud/firebase-firestore.indexes.json` to
|
|
23
23
|
* `listQueryIndexes` of its declaration.
|
|
24
|
-
*/
|
|
24
|
+
*/ /**
|
|
25
|
+
* A header's two orders, both `alone` (AGL-3680): served while nothing
|
|
26
|
+
* narrows the list, so on these subcollection lists they cost no composite.
|
|
27
|
+
* Asked under a filter or the search, the plan falls back to the list's
|
|
28
|
+
* default order and its notices say so.
|
|
29
|
+
*/ const aloneSorts = (column, path, label)=>[
|
|
30
|
+
{
|
|
31
|
+
path,
|
|
32
|
+
direction: 'asc',
|
|
33
|
+
column,
|
|
34
|
+
label,
|
|
35
|
+
alone: true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
path,
|
|
39
|
+
direction: 'desc',
|
|
40
|
+
column,
|
|
41
|
+
label,
|
|
42
|
+
alone: true
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
/*==========================================
|
|
25
46
|
* SUPPRESSIONS — `hosts/{hostId}/suppressions`, newest first.
|
|
26
47
|
*
|
|
27
48
|
* `emailTokens` is `emailSearchTokens(email)`, stamped by every writer (the
|
|
@@ -33,6 +54,10 @@
|
|
|
33
54
|
*
|
|
34
55
|
* `Since` is `createdAt`, the list's own order, so a date range on it
|
|
35
56
|
* keeps that order and needs no index of its own.
|
|
57
|
+
*
|
|
58
|
+
* Every header sorts the query (AGL-3680). `email` is on every row — the
|
|
59
|
+
* address, or `null` for an erasure — and the same backfill stamps `null` on
|
|
60
|
+
* a row from before addresses were stored, so an Address order drops none.
|
|
36
61
|
*=========================================*/ export const SUPPRESSION_LIST_QUERY = {
|
|
37
62
|
fields: [
|
|
38
63
|
{
|
|
@@ -64,8 +89,18 @@
|
|
|
64
89
|
{
|
|
65
90
|
path: 'createdAt',
|
|
66
91
|
direction: 'desc',
|
|
67
|
-
column: 'since'
|
|
68
|
-
|
|
92
|
+
column: 'since',
|
|
93
|
+
label: 'Since'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
path: 'createdAt',
|
|
97
|
+
direction: 'asc',
|
|
98
|
+
column: 'since',
|
|
99
|
+
label: 'Since',
|
|
100
|
+
alone: true
|
|
101
|
+
},
|
|
102
|
+
...aloneSorts('email', 'email', 'Address'),
|
|
103
|
+
...aloneSorts('reason', 'reason', 'Reason')
|
|
69
104
|
],
|
|
70
105
|
search: {
|
|
71
106
|
tokensPath: 'emailTokens'
|
|
@@ -84,7 +119,8 @@ export const SUPPRESSION_FILTER_HEADERS = {
|
|
|
84
119
|
* `backfill-email-list-filters.mjs` stamps the lists written before them.
|
|
85
120
|
* `kind` is written on every create; the same backfill stamps `manual` on a
|
|
86
121
|
* list that predates the field, which is what an absent kind has always
|
|
87
|
-
* meant.
|
|
122
|
+
* meant. The materializer writes `dynamic`, so Membership sorts the query
|
|
123
|
+
* (AGL-3680). Subscribers is a count read per row: it sorts the page.
|
|
88
124
|
*=========================================*/ export const EMAIL_LIST_QUERY = {
|
|
89
125
|
fields: [
|
|
90
126
|
{
|
|
@@ -111,8 +147,17 @@ export const SUPPRESSION_FILTER_HEADERS = {
|
|
|
111
147
|
{
|
|
112
148
|
path: 'name',
|
|
113
149
|
direction: 'asc',
|
|
114
|
-
column: 'name'
|
|
115
|
-
|
|
150
|
+
column: 'name',
|
|
151
|
+
label: 'List'
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
path: 'name',
|
|
155
|
+
direction: 'desc',
|
|
156
|
+
column: 'name',
|
|
157
|
+
label: 'List',
|
|
158
|
+
alone: true
|
|
159
|
+
},
|
|
160
|
+
...aloneSorts('kind', 'kind', 'Membership')
|
|
116
161
|
],
|
|
117
162
|
search: {
|
|
118
163
|
tokensPath: 'nameTokens'
|
|
@@ -149,7 +194,14 @@ export const EMAIL_LIST_FILTER_OPTIONS = {
|
|
|
149
194
|
* `manual` where it is absent, which is what absent has always meant.
|
|
150
195
|
*
|
|
151
196
|
* ⛔ Consent is NOT filterable. It is read per viewing consent group
|
|
152
|
-
* (`readMarketingBasis`), so no stored value answers it for every reader
|
|
197
|
+
* (`readMarketingBasis`), so no stored value answers it for every reader —
|
|
198
|
+
* its header sorts the page.
|
|
199
|
+
*
|
|
200
|
+
* Address, Name, Joined and How sort the query (AGL-3680), each `alone`.
|
|
201
|
+
* `enrollListMember` stamps `name` (null without one) and `addedAt` on every
|
|
202
|
+
* row it writes — a legacy row lacking `addedAt` gets its document's create
|
|
203
|
+
* time — and `backfill-list-member-sort-fields.mjs` stamps both on the rows
|
|
204
|
+
* written before, so neither order drops a member.
|
|
153
205
|
*=========================================*/ export const LIST_MEMBER_QUERY = {
|
|
154
206
|
fields: [
|
|
155
207
|
{
|
|
@@ -175,7 +227,11 @@ export const EMAIL_LIST_FILTER_OPTIONS = {
|
|
|
175
227
|
{
|
|
176
228
|
path: '__name__',
|
|
177
229
|
direction: 'asc'
|
|
178
|
-
}
|
|
230
|
+
},
|
|
231
|
+
...aloneSorts('email', 'email', 'Address'),
|
|
232
|
+
...aloneSorts('name', 'name', 'Name'),
|
|
233
|
+
...aloneSorts('addedAt', 'addedAt', 'Joined'),
|
|
234
|
+
...aloneSorts('via', 'via', 'How')
|
|
179
235
|
],
|
|
180
236
|
search: {
|
|
181
237
|
tokensPath: 'searchTokens'
|
|
@@ -235,7 +291,15 @@ export const LIST_MEMBER_FILTER_OPTIONS = {
|
|
|
235
291
|
{
|
|
236
292
|
path: 'nameLower',
|
|
237
293
|
direction: 'asc',
|
|
238
|
-
column: 'displayName'
|
|
294
|
+
column: 'displayName',
|
|
295
|
+
label: 'Template'
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
path: 'nameLower',
|
|
299
|
+
direction: 'desc',
|
|
300
|
+
column: 'displayName',
|
|
301
|
+
label: 'Template',
|
|
302
|
+
alone: true
|
|
239
303
|
}
|
|
240
304
|
],
|
|
241
305
|
search: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/constants/list-queries.ts"],"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\nimport type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * WHAT EACH EMAILS LIST CAN BE ASKED, ON ITS FIRESTORE QUERY (AGL-3321).\n *\n * Every clause the Filters panel offers and every quick-search word is a\n * predicate on the list's query, planned by `planListQuery`, so each page is a\n * page of the answer. A question no query can answer is not offered. Each\n * list's spec holds `cloud/firebase-firestore.indexes.json` to\n * `listQueryIndexes` of its declaration.\n */\n\n/*==========================================\n * SUPPRESSIONS — `hosts/{hostId}/suppressions`, newest first.\n *\n * `emailTokens` is `emailSearchTokens(email)`, stamped by every writer (the\n * unsubscribe link, the bounce and complaint webhook, the Add drawer, the\n * consent-group carry) and by `backfill-host-suppression-filters.mjs` on\n * the rows written before it. `reason` is written by every writer, and the\n * same backfill stamps `unsubscribe` on the pre-AGL-2408 rows that carry\n * none, which is what an absent reason has always meant.\n *\n * `Since` is `createdAt`, the list's own order, so a date range on it\n * keeps that order and needs no index of its own.\n *=========================================*/\nexport const SUPPRESSION_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'email',\n kind: 'text',\n path: 'email',\n tokensPath: 'emailTokens',\n operators: ['contains'],\n },\n { column: 'reason', kind: 'exact', path: 'reason', operators: ['equals', 'isAnyOf'] },\n { column: 'since', kind: 'date', path: 'createdAt', presence: 'always' },\n ],\n sorts: [{ path: 'createdAt', direction: 'desc', column: 'since' }],\n search: { tokensPath: 'emailTokens' },\n}\n\nexport const SUPPRESSION_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Address',\n reason: 'Reason',\n since: 'Since',\n}\n\n/*==========================================\n * EMAIL LISTS — `orgs/{orgId}/lists`, by name.\n *\n * `nameLower` and `nameTokens` travel with `name` (`nameSearchFields`) on\n * the two writers — the card's create and the edit page's rename — and\n * `backfill-email-list-filters.mjs` stamps the lists written before them.\n * `kind` is written on every create; the same backfill stamps `manual` on a\n * list that predates the field, which is what an absent kind has always\n * meant.\n *=========================================*/\nexport const EMAIL_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n operators: ['contains', 'equals'],\n },\n { column: 'kind', kind: 'exact', path: 'kind', operators: ['equals'] },\n ],\n sorts: [{ path: 'name', direction: 'asc', column: 'name' }],\n search: { tokensPath: 'nameTokens' },\n}\n\nexport const EMAIL_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'List',\n kind: 'Membership',\n}\n\nexport const EMAIL_LIST_FILTER_OPTIONS: Readonly<Record<string, readonly ListFilterOption[]>> = {\n kind: [\n { value: 'manual', label: 'Manual' },\n { value: 'dynamic', label: 'Rule' },\n ],\n}\n\n/*==========================================\n * A LIST'S MEMBERS — `orgs/{orgId}/lists/{listId}/members`, by document id.\n *\n * `__name__` is the order because it is the one key every row has (see the\n * panel), and under it equalities and one array clause merge on Firestore's\n * automatic single-field indexes: no composite at all.\n *\n * `searchTokens` is the name's word prefixes and the address's\n * (`listMemberSearchTokens`), stamped by `enrollListMember` — the\n * collection's only writer — and by `backfill-email-list-filters.mjs`.\n * `email` is normalized before every write, so `equals` asks for the\n * normalized address. `via` is written on every new row and stamped\n * `manual` where it is absent, which is what absent has always meant.\n *\n * ⛔ Consent is NOT filterable. It is read per viewing consent group\n * (`readMarketingBasis`), so no stored value answers it for every reader.\n *=========================================*/\nexport const LIST_MEMBER_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals'],\n },\n { column: 'via', kind: 'exact', path: 'via', operators: ['equals'] },\n ],\n sorts: [{ path: '__name__', direction: 'asc' }],\n search: { tokensPath: 'searchTokens' },\n}\n\nexport const LIST_MEMBER_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Address',\n via: 'How',\n}\n\nexport const LIST_MEMBER_FILTER_OPTIONS: Readonly<Record<string, readonly ListFilterOption[]>> = {\n via: [\n { value: 'rule', label: 'Rule' },\n { value: 'manual', label: 'Added' },\n ],\n}\n\n/*==========================================\n * EMAIL TEMPLATES — `hosts/{hostId}/screens` where `kind == 'email'`, by\n * name.\n *\n * The screen search keys — `nameLower` and `nameTokens`, both from\n * `displayName` (`displayNameSearchFields`) — are what every screen writer\n * stamps on a create and a rename, and what the site artifacts backfill\n * stamps on the screens written before them. The order is `nameLower`, so a\n * screen without it is not listed: a deleted template clears its keys with\n * the delete (`emailTemplateSoftDelete`), which is how the list leaves\n * tombstones out without a clause no query could hold (\"deletedAt is\n * absent\").\n *\n * ⛔ Origin (yours or installed) is NOT filterable: \"installed\" is the\n * presence of the install provenance and \"yours\" its absence, and a query\n * cannot ask for a field to be absent.\n *=========================================*/\nexport const EMAIL_TEMPLATE_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'displayName',\n kind: 'text',\n path: 'displayName',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n // `startsWith` ranges over `nameLower`, the list's own order. No\n // `endsWith`: it would range over `nameReversed` and reorder the list\n // by it, an order's worth of indexes for one operator.\n operators: ['contains', 'equals', 'startsWith'],\n },\n ],\n sorts: [{ path: 'nameLower', direction: 'asc', column: 'displayName' }],\n search: { tokensPath: 'nameTokens' },\n}\n\n/** The template list's scope: email screens only. */\nexport const EMAIL_TEMPLATE_BASE = [{ path: 'kind', op: '==' as const, value: 'email' }]\n\nexport const EMAIL_TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Template',\n}\n"],"names":["SUPPRESSION_LIST_QUERY","fields","column","kind","path","tokensPath","operators","presence","sorts","direction","search","SUPPRESSION_FILTER_HEADERS","email","reason","since","EMAIL_LIST_QUERY","lowerPath","EMAIL_LIST_FILTER_HEADERS","name","EMAIL_LIST_FILTER_OPTIONS","value","label","LIST_MEMBER_QUERY","LIST_MEMBER_FILTER_HEADERS","via","LIST_MEMBER_FILTER_OPTIONS","EMAIL_TEMPLATE_QUERY","EMAIL_TEMPLATE_BASE","op","EMAIL_TEMPLATE_FILTER_HEADERS","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAKD;;;;;;;;CAQC,GAED;;;;;;;;;;;;2CAY2C,GAC3C,OAAO,MAAMA,yBAA+C;IAC1DC,QAAQ;QACN;YACEC,QAAQ;YACRC,MAAM;YACNC,MAAM;YACNC,YAAY;YACZC,WAAW;gBAAC;aAAW;QACzB;QACA;YAAEJ,QAAQ;YAAUC,MAAM;YAASC,MAAM;YAAUE,WAAW;gBAAC;gBAAU;aAAU;QAAC;QACpF;YAAEJ,QAAQ;YAASC,MAAM;YAAQC,MAAM;YAAaG,UAAU;QAAS;KACxE;IACDC,OAAO;QAAC;YAAEJ,MAAM;YAAaK,WAAW;YAAQP,QAAQ;QAAQ;KAAE;IAClEQ,QAAQ;QAAEL,YAAY;IAAc;AACtC,EAAC;AAED,OAAO,MAAMM,6BAA+D;IAC1EC,OAAO;IACPC,QAAQ;IACRC,OAAO;AACT,EAAC;AAED;;;;;;;;;2CAS2C,GAC3C,OAAO,MAAMC,mBAAyC;IACpDd,QAAQ;QACN;YACEC,QAAQ;YACRC,MAAM;YACNC,MAAM;YACNY,WAAW;YACXX,YAAY;YACZC,WAAW;gBAAC;gBAAY;aAAS;QACnC;QACA;YAAEJ,QAAQ;YAAQC,MAAM;YAASC,MAAM;YAAQE,WAAW;gBAAC;aAAS;QAAC;KACtE;IACDE,OAAO;QAAC;YAAEJ,MAAM;YAAQK,WAAW;YAAOP,QAAQ;QAAO;KAAE;IAC3DQ,QAAQ;QAAEL,YAAY;IAAa;AACrC,EAAC;AAED,OAAO,MAAMY,4BAA8D;IACzEC,MAAM;IACNf,MAAM;AACR,EAAC;AAED,OAAO,MAAMgB,4BAAmF;IAC9FhB,MAAM;QACJ;YAAEiB,OAAO;YAAUC,OAAO;QAAS;QACnC;YAAED,OAAO;YAAWC,OAAO;QAAO;KACnC;AACH,EAAC;AAED;;;;;;;;;;;;;;;;2CAgB2C,GAC3C,OAAO,MAAMC,oBAA0C;IACrDrB,QAAQ;QACN;YACEC,QAAQ;YACRC,MAAM;YACNC,MAAM;YACNY,WAAW;YACXT,UAAU;YACVD,WAAW;gBAAC;aAAS;QACvB;QACA;YAAEJ,QAAQ;YAAOC,MAAM;YAASC,MAAM;YAAOE,WAAW;gBAAC;aAAS;QAAC;KACpE;IACDE,OAAO;QAAC;YAAEJ,MAAM;YAAYK,WAAW;QAAM;KAAE;IAC/CC,QAAQ;QAAEL,YAAY;IAAe;AACvC,EAAC;AAED,OAAO,MAAMkB,6BAA+D;IAC1EX,OAAO;IACPY,KAAK;AACP,EAAC;AAED,OAAO,MAAMC,6BAAoF;IAC/FD,KAAK;QACH;YAAEJ,OAAO;YAAQC,OAAO;QAAO;QAC/B;YAAED,OAAO;YAAUC,OAAO;QAAQ;KACnC;AACH,EAAC;AAED;;;;;;;;;;;;;;;;2CAgB2C,GAC3C,OAAO,MAAMK,uBAA6C;IACxDzB,QAAQ;QACN;YACEC,QAAQ;YACRC,MAAM;YACNC,MAAM;YACNY,WAAW;YACXX,YAAY;YACZ,iEAAiE;YACjE,sEAAsE;YACtE,uDAAuD;YACvDC,WAAW;gBAAC;gBAAY;gBAAU;aAAa;QACjD;KACD;IACDE,OAAO;QAAC;YAAEJ,MAAM;YAAaK,WAAW;YAAOP,QAAQ;QAAc;KAAE;IACvEQ,QAAQ;QAAEL,YAAY;IAAa;AACrC,EAAC;AAED,mDAAmD,GACnD,OAAO,MAAMsB,sBAAsB;IAAC;QAAEvB,MAAM;QAAQwB,IAAI;QAAeR,OAAO;IAAQ;CAAE,CAAA;AAExF,OAAO,MAAMS,gCAAkE;IAC7EC,aAAa;AACf,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/constants/list-queries.ts"],"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\nimport type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type {\n ListQueryDeclaration,\n ListQuerySort,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * WHAT EACH EMAILS LIST CAN BE ASKED, ON ITS FIRESTORE QUERY (AGL-3321).\n *\n * Every clause the Filters panel offers and every quick-search word is a\n * predicate on the list's query, planned by `planListQuery`, so each page is a\n * page of the answer. A question no query can answer is not offered. Each\n * list's spec holds `cloud/firebase-firestore.indexes.json` to\n * `listQueryIndexes` of its declaration.\n */\n\n/**\n * A header's two orders, both `alone` (AGL-3680): served while nothing\n * narrows the list, so on these subcollection lists they cost no composite.\n * Asked under a filter or the search, the plan falls back to the list's\n * default order and its notices say so.\n */\nconst aloneSorts = (column: string, path: string, label: string): ListQuerySort[] => [\n { path, direction: 'asc', column, label, alone: true },\n { path, direction: 'desc', column, label, alone: true },\n]\n\n/*==========================================\n * SUPPRESSIONS — `hosts/{hostId}/suppressions`, newest first.\n *\n * `emailTokens` is `emailSearchTokens(email)`, stamped by every writer (the\n * unsubscribe link, the bounce and complaint webhook, the Add drawer, the\n * consent-group carry) and by `backfill-host-suppression-filters.mjs` on\n * the rows written before it. `reason` is written by every writer, and the\n * same backfill stamps `unsubscribe` on the pre-AGL-2408 rows that carry\n * none, which is what an absent reason has always meant.\n *\n * `Since` is `createdAt`, the list's own order, so a date range on it\n * keeps that order and needs no index of its own.\n *\n * Every header sorts the query (AGL-3680). `email` is on every row — the\n * address, or `null` for an erasure — and the same backfill stamps `null` on\n * a row from before addresses were stored, so an Address order drops none.\n *=========================================*/\nexport const SUPPRESSION_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'email',\n kind: 'text',\n path: 'email',\n tokensPath: 'emailTokens',\n operators: ['contains'],\n },\n { column: 'reason', kind: 'exact', path: 'reason', operators: ['equals', 'isAnyOf'] },\n { column: 'since', kind: 'date', path: 'createdAt', presence: 'always' },\n ],\n sorts: [\n { path: 'createdAt', direction: 'desc', column: 'since', label: 'Since' },\n { path: 'createdAt', direction: 'asc', column: 'since', label: 'Since', alone: true },\n ...aloneSorts('email', 'email', 'Address'),\n ...aloneSorts('reason', 'reason', 'Reason'),\n ],\n search: { tokensPath: 'emailTokens' },\n}\n\nexport const SUPPRESSION_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Address',\n reason: 'Reason',\n since: 'Since',\n}\n\n/*==========================================\n * EMAIL LISTS — `orgs/{orgId}/lists`, by name.\n *\n * `nameLower` and `nameTokens` travel with `name` (`nameSearchFields`) on\n * the two writers — the card's create and the edit page's rename — and\n * `backfill-email-list-filters.mjs` stamps the lists written before them.\n * `kind` is written on every create; the same backfill stamps `manual` on a\n * list that predates the field, which is what an absent kind has always\n * meant. The materializer writes `dynamic`, so Membership sorts the query\n * (AGL-3680). Subscribers is a count read per row: it sorts the page.\n *=========================================*/\nexport const EMAIL_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n operators: ['contains', 'equals'],\n },\n { column: 'kind', kind: 'exact', path: 'kind', operators: ['equals'] },\n ],\n sorts: [\n { path: 'name', direction: 'asc', column: 'name', label: 'List' },\n { path: 'name', direction: 'desc', column: 'name', label: 'List', alone: true },\n ...aloneSorts('kind', 'kind', 'Membership'),\n ],\n search: { tokensPath: 'nameTokens' },\n}\n\nexport const EMAIL_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'List',\n kind: 'Membership',\n}\n\nexport const EMAIL_LIST_FILTER_OPTIONS: Readonly<Record<string, readonly ListFilterOption[]>> = {\n kind: [\n { value: 'manual', label: 'Manual' },\n { value: 'dynamic', label: 'Rule' },\n ],\n}\n\n/*==========================================\n * A LIST'S MEMBERS — `orgs/{orgId}/lists/{listId}/members`, by document id.\n *\n * `__name__` is the order because it is the one key every row has (see the\n * panel), and under it equalities and one array clause merge on Firestore's\n * automatic single-field indexes: no composite at all.\n *\n * `searchTokens` is the name's word prefixes and the address's\n * (`listMemberSearchTokens`), stamped by `enrollListMember` — the\n * collection's only writer — and by `backfill-email-list-filters.mjs`.\n * `email` is normalized before every write, so `equals` asks for the\n * normalized address. `via` is written on every new row and stamped\n * `manual` where it is absent, which is what absent has always meant.\n *\n * ⛔ Consent is NOT filterable. It is read per viewing consent group\n * (`readMarketingBasis`), so no stored value answers it for every reader —\n * its header sorts the page.\n *\n * Address, Name, Joined and How sort the query (AGL-3680), each `alone`.\n * `enrollListMember` stamps `name` (null without one) and `addedAt` on every\n * row it writes — a legacy row lacking `addedAt` gets its document's create\n * time — and `backfill-list-member-sort-fields.mjs` stamps both on the rows\n * written before, so neither order drops a member.\n *=========================================*/\nexport const LIST_MEMBER_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals'],\n },\n { column: 'via', kind: 'exact', path: 'via', operators: ['equals'] },\n ],\n sorts: [\n { path: '__name__', direction: 'asc' },\n ...aloneSorts('email', 'email', 'Address'),\n ...aloneSorts('name', 'name', 'Name'),\n ...aloneSorts('addedAt', 'addedAt', 'Joined'),\n ...aloneSorts('via', 'via', 'How'),\n ],\n search: { tokensPath: 'searchTokens' },\n}\n\nexport const LIST_MEMBER_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Address',\n via: 'How',\n}\n\nexport const LIST_MEMBER_FILTER_OPTIONS: Readonly<Record<string, readonly ListFilterOption[]>> = {\n via: [\n { value: 'rule', label: 'Rule' },\n { value: 'manual', label: 'Added' },\n ],\n}\n\n/*==========================================\n * EMAIL TEMPLATES — `hosts/{hostId}/screens` where `kind == 'email'`, by\n * name.\n *\n * The screen search keys — `nameLower` and `nameTokens`, both from\n * `displayName` (`displayNameSearchFields`) — are what every screen writer\n * stamps on a create and a rename, and what the site artifacts backfill\n * stamps on the screens written before them. The order is `nameLower`, so a\n * screen without it is not listed: a deleted template clears its keys with\n * the delete (`emailTemplateSoftDelete`), which is how the list leaves\n * tombstones out without a clause no query could hold (\"deletedAt is\n * absent\").\n *\n * ⛔ Origin (yours or installed) is NOT filterable: \"installed\" is the\n * presence of the install provenance and \"yours\" its absence, and a query\n * cannot ask for a field to be absent.\n *=========================================*/\nexport const EMAIL_TEMPLATE_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'displayName',\n kind: 'text',\n path: 'displayName',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n // `startsWith` ranges over `nameLower`, the list's own order. No\n // `endsWith`: it would range over `nameReversed` and reorder the list\n // by it, an order's worth of indexes for one operator.\n operators: ['contains', 'equals', 'startsWith'],\n },\n ],\n sorts: [\n { path: 'nameLower', direction: 'asc', column: 'displayName', label: 'Template' },\n {\n path: 'nameLower',\n direction: 'desc',\n column: 'displayName',\n label: 'Template',\n alone: true,\n },\n ],\n search: { tokensPath: 'nameTokens' },\n}\n\n/** The template list's scope: email screens only. */\nexport const EMAIL_TEMPLATE_BASE = [{ path: 'kind', op: '==' as const, value: 'email' }]\n\nexport const EMAIL_TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Template',\n}\n"],"names":["aloneSorts","column","path","label","direction","alone","SUPPRESSION_LIST_QUERY","fields","kind","tokensPath","operators","presence","sorts","search","SUPPRESSION_FILTER_HEADERS","email","reason","since","EMAIL_LIST_QUERY","lowerPath","EMAIL_LIST_FILTER_HEADERS","name","EMAIL_LIST_FILTER_OPTIONS","value","LIST_MEMBER_QUERY","LIST_MEMBER_FILTER_HEADERS","via","LIST_MEMBER_FILTER_OPTIONS","EMAIL_TEMPLATE_QUERY","EMAIL_TEMPLATE_BASE","op","EMAIL_TEMPLATE_FILTER_HEADERS","displayName"],"mappings":"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"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { SCREEN_KIND_EMAIL } from '@aglyn/aglyn/app-utils/screen-
|
|
17
|
+
import { SCREEN_KIND_EMAIL } from '@aglyn/aglyn/app-utils/screen-kind';
|
|
18
18
|
/**
|
|
19
19
|
* THE TWO DOCUMENTS AN EMAIL DESIGN IS: a screen with `kind: 'email'`, and its
|
|
20
20
|
* first version holding the node map.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import { SCREEN_KIND_EMAIL } from "@aglyn/aglyn/app-utils/screen-
|
|
16
|
+
*/ import { SCREEN_KIND_EMAIL } from "@aglyn/aglyn/app-utils/screen-kind";
|
|
17
17
|
import { CANVAS_ROOT_ELEMENT_ID } from "@aglyn/aglyn/foundation/constants/canvas";
|
|
18
18
|
/** A new design's first canvas: one email section holding a greeting. */ export function emailDesignStarterNodes(ids) {
|
|
19
19
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/model/email-design-document.ts"],"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\nimport { SCREEN_KIND_EMAIL } from '@aglyn/aglyn/app-utils/screen-
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/model/email-design-document.ts"],"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\nimport { SCREEN_KIND_EMAIL } from '@aglyn/aglyn/app-utils/screen-kind'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\n\n/**\n * THE TWO DOCUMENTS AN EMAIL DESIGN IS: a screen with `kind: 'email'`, and its\n * first version holding the node map.\n *\n * Leaf modules only, so the console's create (a client component) and the\n * server's draft writer build the same pair from one function: a design made\n * on either side opens in the same besigner, lists on the same Templates tab,\n * and is picked up by the same campaign composer.\n */\n\n/** The screen document of a new email design. */\nexport interface EmailDesignScreenDocument {\n displayName: string\n kind: typeof SCREEN_KIND_EMAIL\n versionId: string\n}\n\n/** The first version of a new email design. */\nexport interface EmailDesignVersionDocument {\n screenId: string\n nodes: Record<string, unknown>\n}\n\n/** A new design's first canvas: one email section holding a greeting. */\nexport function emailDesignStarterNodes(ids: {\n sectionId: string\n textId: string\n}): Record<string, unknown> {\n return {\n [CANVAS_ROOT_ELEMENT_ID]: {\n $id: CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: [ids.sectionId],\n },\n [ids.sectionId]: {\n $id: ids.sectionId,\n componentId: 'emailSection',\n pluginId: 'email',\n parentId: CANVAS_ROOT_ELEMENT_ID,\n nodes: [ids.textId],\n },\n [ids.textId]: {\n $id: ids.textId,\n componentId: 'emailText',\n pluginId: 'email',\n parentId: ids.sectionId,\n props: {\n children: 'Hello {{contact.firstName}},',\n variant: 'body',\n },\n },\n }\n}\n\n/** The screen and first version a design with these ids and this canvas is stored as. */\nexport function emailDesignDocuments(input: {\n screenId: string\n versionId: string\n displayName: string\n nodes: Record<string, unknown>\n}): { screen: EmailDesignScreenDocument; version: EmailDesignVersionDocument } {\n return {\n screen: {\n displayName: input.displayName,\n kind: SCREEN_KIND_EMAIL,\n versionId: input.versionId,\n },\n version: { screenId: input.screenId, nodes: input.nodes },\n }\n}\n"],"names":["SCREEN_KIND_EMAIL","CANVAS_ROOT_ELEMENT_ID","emailDesignStarterNodes","ids","$id","componentId","nodes","sectionId","pluginId","parentId","textId","props","children","variant","emailDesignDocuments","input","screen","displayName","kind","versionId","version","screenId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,qCAAoC;AACtE,SAASC,sBAAsB,QAAQ,2CAA0C;AAyBjF,uEAAuE,GACvE,OAAO,SAASC,wBAAwBC,GAGvC;IACC,OAAO;QACL,CAACF,uBAAuB,EAAE;YACxBG,KAAKH;YACLI,aAAa;YACbC,OAAO;gBAACH,IAAII,SAAS;aAAC;QACxB;QACA,CAACJ,IAAII,SAAS,CAAC,EAAE;YACfH,KAAKD,IAAII,SAAS;YAClBF,aAAa;YACbG,UAAU;YACVC,UAAUR;YACVK,OAAO;gBAACH,IAAIO,MAAM;aAAC;QACrB;QACA,CAACP,IAAIO,MAAM,CAAC,EAAE;YACZN,KAAKD,IAAIO,MAAM;YACfL,aAAa;YACbG,UAAU;YACVC,UAAUN,IAAII,SAAS;YACvBI,OAAO;gBACLC,UAAU;gBACVC,SAAS;YACX;QACF;IACF;AACF;AAEA,uFAAuF,GACvF,OAAO,SAASC,qBAAqBC,KAKpC;IACC,OAAO;QACLC,QAAQ;YACNC,aAAaF,MAAME,WAAW;YAC9BC,MAAMlB;YACNmB,WAAWJ,MAAMI,SAAS;QAC5B;QACAC,SAAS;YAAEC,UAAUN,MAAMM,QAAQ;YAAEf,OAAOS,MAAMT,KAAK;QAAC;IAC1D;AACF"}
|