@aglyn/plugins-marketing 1.0.0-beta.199 → 1.0.0-beta.201
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 +15 -15
- package/src/lib/components/campaign-composer.js +14 -6
- package/src/lib/components/campaign-composer.js.map +1 -1
- package/src/lib/components/campaign-conversions-card.js +6 -0
- package/src/lib/components/campaign-conversions-card.js.map +1 -1
- package/src/lib/components/campaign-detail-card.js +107 -37
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-members-section.d.ts +16 -4
- package/src/lib/components/campaign-members-section.js +171 -105
- package/src/lib/components/campaign-members-section.js.map +1 -1
- package/src/lib/components/campaign-queries.d.ts +11 -0
- package/src/lib/components/campaign-queries.js +13 -0
- package/src/lib/components/campaign-queries.js.map +1 -1
- package/src/lib/components/campaigns-card.d.ts +8 -6
- package/src/lib/components/campaigns-card.js +296 -128
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/email-compose-card.js +4 -3
- package/src/lib/components/email-compose-card.js.map +1 -1
- package/src/lib/components/email-detail.js +16 -5
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/components/email-recipients-card.js +3 -1
- package/src/lib/components/email-recipients-card.js.map +1 -1
- package/src/lib/components/emails-list-card.d.ts +11 -15
- package/src/lib/components/emails-list-card.js +142 -143
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-experiments-card.component.js +110 -78
- package/src/lib/components/host-experiments-card.component.js.map +1 -1
- package/src/lib/components/org-experiments-card.js +7 -3
- package/src/lib/components/org-experiments-card.js.map +1 -1
- package/src/lib/model/campaign-list-query.d.ts +99 -0
- package/src/lib/model/campaign-list-query.js +373 -0
- package/src/lib/model/campaign-list-query.js.map +1 -0
- package/src/lib/model/campaign-members-query.d.ts +37 -0
- package/src/lib/model/campaign-members-query.js +120 -0
- package/src/lib/model/campaign-members-query.js.map +1 -0
- package/src/lib/model/experiment-list-query.d.ts +22 -0
- package/src/lib/model/experiment-list-query.js +85 -0
- package/src/lib/model/experiment-list-query.js.map +1 -0
- package/src/lib/server/campaign-form-flags.d.ts +35 -0
- package/src/lib/server/campaign-form-flags.js +52 -0
- package/src/lib/server/campaign-form-flags.js.map +1 -0
- package/src/lib/server/campaign-manage.d.ts +1 -1
- package/src/lib/server/campaign-manage.js +42 -20
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-send.d.ts +42 -1
- package/src/lib/server/campaign-send.js +247 -16
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/email-events.js +13 -1
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/components/email-send-filter.d.ts +0 -32
- package/src/lib/components/email-send-filter.js +0 -55
- package/src/lib/components/email-send-filter.js.map +0 -1
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
*/ /*
|
|
17
|
+
* WHAT A SITE'S A/B TESTING LIST ASKS FIRESTORE (AGL-3321).
|
|
18
|
+
*
|
|
19
|
+
* Every clause the list's Filters panel offers, and its search box, is a
|
|
20
|
+
* predicate on ONE query over `hosts/{hostId}/experiments`, ordered by name,
|
|
21
|
+
* paged by that query — never a match over the experiments a read happened
|
|
22
|
+
* to hold. Each field is here because a query can serve it:
|
|
23
|
+
*
|
|
24
|
+
* name `contains` a word (the `nameTokens` array) or `equals` the whole
|
|
25
|
+
* name (`nameLower`). Every writer of an experiment stamps both
|
|
26
|
+
* through `nameSearchFields` — the console's editor, the besigner's
|
|
27
|
+
* "test this section" shortcut and the seeds — and
|
|
28
|
+
* `tools/scripts/backfill-experiments-name-search.mjs` stamps the
|
|
29
|
+
* ones saved before;
|
|
30
|
+
* target what the test splits (screen, section, email), which every
|
|
31
|
+
* writer sets;
|
|
32
|
+
* status draft, running, paused or done, which every writer sets.
|
|
33
|
+
*
|
|
34
|
+
* The one order is `name` ascending, as the list has always read. No range
|
|
35
|
+
* is offered: a range orders the list by the field it ranges over, and would
|
|
36
|
+
* cost one composite per field above under that order (four), for a list a
|
|
37
|
+
* site keeps a handful of tests in.
|
|
38
|
+
*/ /** The array `contains` and the search box read: every word's prefixes. */ export const EXPERIMENT_NAME_TOKENS = 'nameTokens';
|
|
39
|
+
/** The lower-cased whole name `equals` reads. */ export const EXPERIMENT_NAME_LOWER = 'nameLower';
|
|
40
|
+
export const EXPERIMENT_LIST_QUERY = {
|
|
41
|
+
fields: [
|
|
42
|
+
{
|
|
43
|
+
column: 'name',
|
|
44
|
+
kind: 'text',
|
|
45
|
+
path: 'name',
|
|
46
|
+
lowerPath: EXPERIMENT_NAME_LOWER,
|
|
47
|
+
tokensPath: EXPERIMENT_NAME_TOKENS,
|
|
48
|
+
operators: [
|
|
49
|
+
'contains',
|
|
50
|
+
'equals'
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
column: 'target',
|
|
55
|
+
kind: 'exact',
|
|
56
|
+
path: 'target',
|
|
57
|
+
presence: 'always',
|
|
58
|
+
operators: [
|
|
59
|
+
'equals',
|
|
60
|
+
'isAnyOf'
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
column: 'status',
|
|
65
|
+
kind: 'exact',
|
|
66
|
+
path: 'status',
|
|
67
|
+
presence: 'always',
|
|
68
|
+
operators: [
|
|
69
|
+
'equals',
|
|
70
|
+
'isAnyOf'
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
sorts: [
|
|
75
|
+
{
|
|
76
|
+
path: 'name',
|
|
77
|
+
direction: 'asc'
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
search: {
|
|
81
|
+
tokensPath: EXPERIMENT_NAME_TOKENS
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=experiment-list-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/experiment-list-query.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 { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * WHAT A SITE'S A/B TESTING LIST ASKS FIRESTORE (AGL-3321).\n *\n * Every clause the list's Filters panel offers, and its search box, is a\n * predicate on ONE query over `hosts/{hostId}/experiments`, ordered by name,\n * paged by that query — never a match over the experiments a read happened\n * to hold. Each field is here because a query can serve it:\n *\n * name `contains` a word (the `nameTokens` array) or `equals` the whole\n * name (`nameLower`). Every writer of an experiment stamps both\n * through `nameSearchFields` — the console's editor, the besigner's\n * \"test this section\" shortcut and the seeds — and\n * `tools/scripts/backfill-experiments-name-search.mjs` stamps the\n * ones saved before;\n * target what the test splits (screen, section, email), which every\n * writer sets;\n * status draft, running, paused or done, which every writer sets.\n *\n * The one order is `name` ascending, as the list has always read. No range\n * is offered: a range orders the list by the field it ranges over, and would\n * cost one composite per field above under that order (four), for a list a\n * site keeps a handful of tests in.\n */\n\n/** The array `contains` and the search box read: every word's prefixes. */\nexport const EXPERIMENT_NAME_TOKENS = 'nameTokens'\n/** The lower-cased whole name `equals` reads. */\nexport const EXPERIMENT_NAME_LOWER = 'nameLower'\n\nexport const EXPERIMENT_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: EXPERIMENT_NAME_LOWER,\n tokensPath: EXPERIMENT_NAME_TOKENS,\n operators: ['contains', 'equals'],\n },\n { column: 'target', kind: 'exact', path: 'target', presence: 'always', operators: ['equals', 'isAnyOf'] },\n { column: 'status', kind: 'exact', path: 'status', presence: 'always', operators: ['equals', 'isAnyOf'] },\n ],\n sorts: [{ path: 'name', direction: 'asc' }],\n search: { tokensPath: EXPERIMENT_NAME_TOKENS },\n}\n"],"names":["EXPERIMENT_NAME_TOKENS","EXPERIMENT_NAME_LOWER","EXPERIMENT_LIST_QUERY","fields","column","kind","path","lowerPath","tokensPath","operators","presence","sorts","direction","search"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,yEAAyE,GACzE,OAAO,MAAMA,yBAAyB,aAAY;AAClD,+CAA+C,GAC/C,OAAO,MAAMC,wBAAwB,YAAW;AAEhD,OAAO,MAAMC,wBAA8C;IACzDC,QAAQ;QACN;YACEC,QAAQ;YACRC,MAAM;YACNC,MAAM;YACNC,WAAWN;YACXO,YAAYR;YACZS,WAAW;gBAAC;gBAAY;aAAS;QACnC;QACA;YAAEL,QAAQ;YAAUC,MAAM;YAASC,MAAM;YAAUI,UAAU;YAAUD,WAAW;gBAAC;gBAAU;aAAU;QAAC;QACxG;YAAEL,QAAQ;YAAUC,MAAM;YAASC,MAAM;YAAUI,UAAU;YAAUD,WAAW;gBAAC;gBAAU;aAAU;QAAC;KACzG;IACDE,OAAO;QAAC;YAAEL,MAAM;YAAQM,WAAW;QAAM;KAAE;IAC3CC,QAAQ;QAAEL,YAAYR;IAAuB;AAC/C,EAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* `inCampaign` restamped from what each form's `campaignIds` now holds
|
|
19
|
+
* (AGL-3330) — for the one writer of `campaignIds` that cannot say what it
|
|
20
|
+
* is leaving behind.
|
|
21
|
+
*
|
|
22
|
+
* Every other writer sets the ids through `formCampaignFields`, which
|
|
23
|
+
* returns the boolean beside them. A campaign's deletion takes its one id out
|
|
24
|
+
* of every form holding it with `arrayRemove`, which is what keeps it from
|
|
25
|
+
* clobbering a campaign added to the same form in the meantime — and which
|
|
26
|
+
* means the write does not know whether the array came out empty. So each
|
|
27
|
+
* form is read back inside a transaction and its flag set from the array as
|
|
28
|
+
* it stands, which is exactly what a concurrent picker save would also
|
|
29
|
+
* leave.
|
|
30
|
+
*
|
|
31
|
+
* Idempotent: a form whose flag already agrees is not written.
|
|
32
|
+
*
|
|
33
|
+
* @returns how many forms were restamped.
|
|
34
|
+
*/
|
|
35
|
+
export declare function restampFormsInCampaign(firestore: FirebaseFirestore.Firestore, refs: readonly FirebaseFirestore.DocumentReference[]): Promise<number>;
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
*/ import { formCampaignFields } from "@aglyn/aglyn/app-utils/forms";
|
|
17
|
+
/**
|
|
18
|
+
* `inCampaign` restamped from what each form's `campaignIds` now holds
|
|
19
|
+
* (AGL-3330) — for the one writer of `campaignIds` that cannot say what it
|
|
20
|
+
* is leaving behind.
|
|
21
|
+
*
|
|
22
|
+
* Every other writer sets the ids through `formCampaignFields`, which
|
|
23
|
+
* returns the boolean beside them. A campaign's deletion takes its one id out
|
|
24
|
+
* of every form holding it with `arrayRemove`, which is what keeps it from
|
|
25
|
+
* clobbering a campaign added to the same form in the meantime — and which
|
|
26
|
+
* means the write does not know whether the array came out empty. So each
|
|
27
|
+
* form is read back inside a transaction and its flag set from the array as
|
|
28
|
+
* it stands, which is exactly what a concurrent picker save would also
|
|
29
|
+
* leave.
|
|
30
|
+
*
|
|
31
|
+
* Idempotent: a form whose flag already agrees is not written.
|
|
32
|
+
*
|
|
33
|
+
* @returns how many forms were restamped.
|
|
34
|
+
*/ export async function restampFormsInCampaign(firestore, refs) {
|
|
35
|
+
let restamped = 0;
|
|
36
|
+
for (const formRef of refs){
|
|
37
|
+
const wrote = await firestore.runTransaction(async (tx)=>{
|
|
38
|
+
const form = await tx.get(formRef);
|
|
39
|
+
if (!form.exists) return false;
|
|
40
|
+
const { inCampaign } = formCampaignFields(form.get('campaignIds'));
|
|
41
|
+
if (form.get('inCampaign') === inCampaign) return false;
|
|
42
|
+
tx.update(formRef, {
|
|
43
|
+
inCampaign
|
|
44
|
+
});
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
if (wrote) restamped += 1;
|
|
48
|
+
}
|
|
49
|
+
return restamped;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=campaign-form-flags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/campaign-form-flags.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 { formCampaignFields } from '@aglyn/aglyn/app-utils/forms'\n\n/**\n * `inCampaign` restamped from what each form's `campaignIds` now holds\n * (AGL-3330) — for the one writer of `campaignIds` that cannot say what it\n * is leaving behind.\n *\n * Every other writer sets the ids through `formCampaignFields`, which\n * returns the boolean beside them. A campaign's deletion takes its one id out\n * of every form holding it with `arrayRemove`, which is what keeps it from\n * clobbering a campaign added to the same form in the meantime — and which\n * means the write does not know whether the array came out empty. So each\n * form is read back inside a transaction and its flag set from the array as\n * it stands, which is exactly what a concurrent picker save would also\n * leave.\n *\n * Idempotent: a form whose flag already agrees is not written.\n *\n * @returns how many forms were restamped.\n */\nexport async function restampFormsInCampaign(\n firestore: FirebaseFirestore.Firestore,\n refs: readonly FirebaseFirestore.DocumentReference[],\n): Promise<number> {\n let restamped = 0\n for (const formRef of refs) {\n const wrote = await firestore.runTransaction(async (tx) => {\n const form = await tx.get(formRef)\n if (!form.exists) return false\n const { inCampaign } = formCampaignFields(form.get('campaignIds'))\n if (form.get('inCampaign') === inCampaign) return false\n tx.update(formRef, { inCampaign })\n return true\n })\n if (wrote) restamped += 1\n }\n return restamped\n}\n"],"names":["formCampaignFields","restampFormsInCampaign","firestore","refs","restamped","formRef","wrote","runTransaction","tx","form","get","exists","inCampaign","update"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,+BAA8B;AAEjE;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,eAAeC,uBACpBC,SAAsC,EACtCC,IAAoD;IAEpD,IAAIC,YAAY;IAChB,KAAK,MAAMC,WAAWF,KAAM;QAC1B,MAAMG,QAAQ,MAAMJ,UAAUK,cAAc,CAAC,OAAOC;YAClD,MAAMC,OAAO,MAAMD,GAAGE,GAAG,CAACL;YAC1B,IAAI,CAACI,KAAKE,MAAM,EAAE,OAAO;YACzB,MAAM,EAAEC,UAAU,EAAE,GAAGZ,mBAAmBS,KAAKC,GAAG,CAAC;YACnD,IAAID,KAAKC,GAAG,CAAC,kBAAkBE,YAAY,OAAO;YAClDJ,GAAGK,MAAM,CAACR,SAAS;gBAAEO;YAAW;YAChC,OAAO;QACT;QACA,IAAIN,OAAOF,aAAa;IAC1B;IACA,OAAOA;AACT"}
|
|
@@ -31,7 +31,7 @@ export declare const CAMPAIGN_DRAFT_ROLE_REFUSAL = "Not a site admin or editor";
|
|
|
31
31
|
/** The id of the one email inside a drafted campaign. */
|
|
32
32
|
export declare function campaignDraftEmailId(campaignId: string): string;
|
|
33
33
|
/** Every field a drafted email is written with, and so every field it can hold. */
|
|
34
|
-
export declare const CAMPAIGN_DRAFT_EMAIL_FIELDS: readonly ["subject", "preheader", "templateScreenId", "subjectVariants", "preheaderVariants", "emailCampaignId", "hostId", "visibleTo", "status", "createdAtMs", "draftedAt", "draftedBy"];
|
|
34
|
+
export declare const CAMPAIGN_DRAFT_EMAIL_FIELDS: readonly ["subject", "subjectTokens", "preheader", "templateScreenId", "subjectVariants", "preheaderVariants", "emailCampaignId", "hostId", "visibleTo", "status", "createdAtMs", "draftedAt", "draftedBy"];
|
|
35
35
|
/** What a caller sends as `content`. */
|
|
36
36
|
export interface CampaignDraftContent {
|
|
37
37
|
/** The email design the email sends: a `kind: 'email'` screen on the site. */
|
|
@@ -88,6 +88,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
88
88
|
*/ import firebaseAdmin from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
89
89
|
import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
|
|
90
90
|
import { logResourceDuplicated } from "@aglyn/tenant-data-admin/server/duplicate-activity";
|
|
91
|
+
import { restampCrmListFieldsOf } from "@aglyn/tenant-data-admin/server/crm-records";
|
|
92
|
+
import { restampFormsInCampaign } from "./campaign-form-flags.js";
|
|
91
93
|
import { DUPLICATE_BUSY_MESSAGE, duplicateDisplayName, uniqueDuplicateName } from "@aglyn/aglyn/app-utils/duplicate-resource";
|
|
92
94
|
import { claimAttempt, createResourceUid } from "@aglyn/aglyn/server";
|
|
93
95
|
/*
|
|
@@ -100,6 +102,7 @@ import { CAMPAIGN_MEMBER_HOST_COLLECTIONS, CAMPAIGN_MEMBERSHIP_FIELD, contactCam
|
|
|
100
102
|
import { CAMPAIGN_SEND_CONTAINER_FIELD, CAMPAIGN_SEND_HOST_FIELD, campaignPlacedOnHost, campaignSiteIds, campaignVisibleTo } from "@aglyn/shared-ui-email-campaigns/model";
|
|
101
103
|
import { campaignSendSiteStamp, orgCampaignSends, orgEmailCampaigns, sendHostId, sendIsOnHost } from "./campaign-org-refs.js";
|
|
102
104
|
import { SCREEN_KIND_EMAIL } from "@aglyn/aglyn/app-utils/screen-route";
|
|
105
|
+
import { campaignContainerSearchFields, campaignSendSearchFields } from "../model/campaign-list-query.js";
|
|
103
106
|
import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
104
107
|
import { runPluginMembershipDetachers } from "@aglyn/aglyn/plugin-manager/plugin-membership-detach";
|
|
105
108
|
import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
@@ -132,7 +135,8 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
|
132
135
|
* removing a container some sites' records would still name.
|
|
133
136
|
*/ const ORG_SITES_MAX = 200;
|
|
134
137
|
/**
|
|
135
|
-
*
|
|
138
|
+
* Files every send in a campaign under NO campaign: {@link
|
|
139
|
+
* CAMPAIGN_SEND_CONTAINER_FIELD} set to `null`.
|
|
136
140
|
*
|
|
137
141
|
* Equality on one field with a `limit` — no `orderBy` — which Firestore's
|
|
138
142
|
* automatic single-field index serves. Ordering would need a composite index
|
|
@@ -151,15 +155,14 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
|
151
155
|
const batch = firestore.batch();
|
|
152
156
|
for (const send of page.docs){
|
|
153
157
|
/*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* readers agreeing.
|
|
158
|
+
* `null`, not an empty string and not a removed field. "Belongs to no
|
|
159
|
+
* campaign" has ONE stored shape, because it is a query: the lists ask
|
|
160
|
+
* `emailCampaignId == null` for single sends (AGL-3321), which an
|
|
161
|
+
* empty string would not answer and a missing field cannot — a query
|
|
162
|
+
* never finds a field that is not there. `campaignListRows` adopts any
|
|
163
|
+
* falsy container id, so every reader agrees.
|
|
161
164
|
*/ batch.update(send.ref, {
|
|
162
|
-
[CAMPAIGN_SEND_CONTAINER_FIELD]:
|
|
165
|
+
[CAMPAIGN_SEND_CONTAINER_FIELD]: null
|
|
163
166
|
});
|
|
164
167
|
}
|
|
165
168
|
await batch.commit();
|
|
@@ -198,12 +201,26 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
|
198
201
|
remaining: false
|
|
199
202
|
};
|
|
200
203
|
const batch = firestore.batch();
|
|
204
|
+
const isForms = collectionRef.id === 'forms';
|
|
201
205
|
for (const member of page.docs){
|
|
202
|
-
batch.update(member.ref, {
|
|
206
|
+
batch.update(member.ref, _extends({
|
|
203
207
|
[fieldPath]: firebaseAdmin.firestore.FieldValue.arrayRemove(campaignId)
|
|
204
|
-
}
|
|
208
|
+
}, isForms ? {
|
|
209
|
+
updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
|
|
210
|
+
} : {}));
|
|
205
211
|
}
|
|
206
212
|
await batch.commit();
|
|
213
|
+
// A lead's campaigns are what the Leads list's Campaign filter reads
|
|
214
|
+
// under a site (AGL-3321): restamped from each lead as it now stands.
|
|
215
|
+
if (collectionRef.id === 'leads') {
|
|
216
|
+
await restampCrmListFieldsOf(page.docs.map((member)=>member.ref), 'leads');
|
|
217
|
+
}
|
|
218
|
+
// A form's `inCampaign` is what the Forms list's "In a campaign" filter
|
|
219
|
+
// asks (AGL-3330), and `arrayRemove` cannot say whether it emptied the
|
|
220
|
+
// array: restamped from each form as it now stands.
|
|
221
|
+
if (isForms) {
|
|
222
|
+
await restampFormsInCampaign(firestore, page.docs.map((member)=>member.ref));
|
|
223
|
+
}
|
|
207
224
|
detached += page.size;
|
|
208
225
|
if (page.size < DETACH_BATCH) return {
|
|
209
226
|
detached,
|
|
@@ -549,7 +566,7 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
|
549
566
|
};
|
|
550
567
|
}
|
|
551
568
|
const outcome = await firestore.runTransaction(async (transaction)=>{
|
|
552
|
-
var _source_data, _ref, _data_displayName;
|
|
569
|
+
var _source_data, _ref, _data_displayName, _copy_CAMPAIGN_SEND_CONTAINER_FIELD;
|
|
553
570
|
const [source, siblings] = await Promise.all([
|
|
554
571
|
transaction.get(messages.doc(emailId)),
|
|
555
572
|
transaction.get(messages.select('displayName', 'subject').where(CAMPAIGN_SEND_HOST_FIELD, '==', siteId))
|
|
@@ -573,7 +590,9 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
|
573
590
|
if (data[field] !== undefined && data[field] !== null) copy[field] = data[field];
|
|
574
591
|
}
|
|
575
592
|
const nowMs = Date.now();
|
|
576
|
-
transaction.create(messages.doc(id), _extends({}, copy,
|
|
593
|
+
transaction.create(messages.doc(id), _extends({}, copy, campaignSendSearchFields(copy['subject']), {
|
|
594
|
+
[CAMPAIGN_SEND_CONTAINER_FIELD]: (_copy_CAMPAIGN_SEND_CONTAINER_FIELD = copy[CAMPAIGN_SEND_CONTAINER_FIELD]) != null ? _copy_CAMPAIGN_SEND_CONTAINER_FIELD : null
|
|
595
|
+
}, campaignSendSiteStamp(siteId), {
|
|
577
596
|
displayName: name,
|
|
578
597
|
status: 'draft',
|
|
579
598
|
createdAtMs: nowMs,
|
|
@@ -662,6 +681,7 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
|
662
681
|
}
|
|
663
682
|
/** Every field a drafted email is written with, and so every field it can hold. */ export const CAMPAIGN_DRAFT_EMAIL_FIELDS = [
|
|
664
683
|
'subject',
|
|
684
|
+
'subjectTokens',
|
|
665
685
|
'preheader',
|
|
666
686
|
'templateScreenId',
|
|
667
687
|
'subjectVariants',
|
|
@@ -857,19 +877,21 @@ export function createCampaignDraftWriter(deps = {}) {
|
|
|
857
877
|
const nowMs = request.now.getTime();
|
|
858
878
|
// A site hub's create drawer's own document: a name, no dates, no
|
|
859
879
|
// lists, placed on the site it was drafted for.
|
|
860
|
-
|
|
861
|
-
|
|
880
|
+
const visibleTo = campaignVisibleTo([
|
|
881
|
+
request.hostId
|
|
882
|
+
]);
|
|
883
|
+
transaction.create(campaignRef, _extends({
|
|
884
|
+
name
|
|
885
|
+
}, campaignContainerSearchFields(name), {
|
|
862
886
|
listIds: [],
|
|
863
|
-
visibleTo
|
|
864
|
-
request.hostId
|
|
865
|
-
]),
|
|
887
|
+
visibleTo,
|
|
866
888
|
createdAtMs: nowMs,
|
|
867
889
|
createdBy: request.uid
|
|
868
|
-
});
|
|
890
|
+
}));
|
|
869
891
|
const { value } = read;
|
|
870
892
|
transaction.create(emailRef, _extends({
|
|
871
893
|
subject: value.subject
|
|
872
|
-
}, value.preheader ? {
|
|
894
|
+
}, campaignSendSearchFields(value.subject), value.preheader ? {
|
|
873
895
|
preheader: value.preheader
|
|
874
896
|
} : {}, {
|
|
875
897
|
templateScreenId: value.templateScreenId
|