@aglyn/plugins-marketing 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 +56 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/announcement-bar-card.component.d.ts +20 -0
- package/src/lib/components/announcement-bar-card.component.js +292 -0
- package/src/lib/components/announcement-bar-card.component.js.map +1 -0
- package/src/lib/components/campaign-composer.d.ts +84 -0
- package/src/lib/components/campaign-composer.js +1643 -0
- package/src/lib/components/campaign-composer.js.map +1 -0
- package/src/lib/components/campaign-conversions-card.d.ts +22 -0
- package/src/lib/components/campaign-conversions-card.js +642 -0
- package/src/lib/components/campaign-conversions-card.js.map +1 -0
- package/src/lib/components/campaign-detail-card.d.ts +55 -0
- package/src/lib/components/campaign-detail-card.js +771 -0
- package/src/lib/components/campaign-detail-card.js.map +1 -0
- package/src/lib/components/campaign-edit-drawer.d.ts +60 -0
- package/src/lib/components/campaign-edit-drawer.js +252 -0
- package/src/lib/components/campaign-edit-drawer.js.map +1 -0
- package/src/lib/components/campaign-email-zones.d.ts +111 -0
- package/src/lib/components/campaign-email-zones.js +54 -0
- package/src/lib/components/campaign-email-zones.js.map +1 -0
- package/src/lib/components/campaign-glance-card.component.d.ts +18 -0
- package/src/lib/components/campaign-glance-card.component.js +151 -0
- package/src/lib/components/campaign-glance-card.component.js.map +1 -0
- package/src/lib/components/campaign-members-section.d.ts +87 -0
- package/src/lib/components/campaign-members-section.js +499 -0
- package/src/lib/components/campaign-members-section.js.map +1 -0
- package/src/lib/components/campaign-reach-sections.d.ts +137 -0
- package/src/lib/components/campaign-reach-sections.js +694 -0
- package/src/lib/components/campaign-reach-sections.js.map +1 -0
- package/src/lib/components/campaign-report-card.d.ts +91 -0
- package/src/lib/components/campaign-report-card.js +667 -0
- package/src/lib/components/campaign-report-card.js.map +1 -0
- package/src/lib/components/campaign-test-send-drawer.d.ts +45 -0
- package/src/lib/components/campaign-test-send-drawer.js +238 -0
- package/src/lib/components/campaign-test-send-drawer.js.map +1 -0
- package/src/lib/components/campaigns-card.d.ts +31 -0
- package/src/lib/components/campaigns-card.js +667 -0
- package/src/lib/components/campaigns-card.js.map +1 -0
- package/src/lib/components/conversion-attribution.component.d.ts +62 -0
- package/src/lib/components/conversion-attribution.component.js +127 -0
- package/src/lib/components/conversion-attribution.component.js.map +1 -0
- package/src/lib/components/email-compose-card.d.ts +12 -0
- package/src/lib/components/email-compose-card.js +157 -0
- package/src/lib/components/email-compose-card.js.map +1 -0
- package/src/lib/components/email-detail.d.ts +40 -0
- package/src/lib/components/email-detail.js +1180 -0
- package/src/lib/components/email-detail.js.map +1 -0
- package/src/lib/components/email-edit-drawer.d.ts +54 -0
- package/src/lib/components/email-edit-drawer.js +172 -0
- package/src/lib/components/email-edit-drawer.js.map +1 -0
- package/src/lib/components/email-messages-widget.d.ts +22 -0
- package/src/lib/components/email-messages-widget.js +49 -0
- package/src/lib/components/email-messages-widget.js.map +1 -0
- package/src/lib/components/email-recipients-card.d.ts +50 -0
- package/src/lib/components/email-recipients-card.js +338 -0
- package/src/lib/components/email-recipients-card.js.map +1 -0
- package/src/lib/components/emails-list-card.d.ts +36 -0
- package/src/lib/components/emails-list-card.js +544 -0
- package/src/lib/components/emails-list-card.js.map +1 -0
- package/src/lib/components/experiment-zones.d.ts +78 -0
- package/src/lib/components/experiment-zones.js +65 -0
- package/src/lib/components/experiment-zones.js.map +1 -0
- package/src/lib/components/host-experiments-card.component.d.ts +17 -0
- package/src/lib/components/host-experiments-card.component.js +887 -0
- package/src/lib/components/host-experiments-card.component.js.map +1 -0
- package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
- package/src/lib/components/host-marketing-summary-card.component.js +161 -0
- package/src/lib/components/host-marketing-summary-card.component.js.map +1 -0
- package/src/lib/components/host-overlays-card.component.d.ts +19 -0
- package/src/lib/components/host-overlays-card.component.js +783 -0
- package/src/lib/components/host-overlays-card.component.js.map +1 -0
- package/src/lib/components/marketing-console-page.d.ts +28 -0
- package/src/lib/components/marketing-console-page.js +166 -0
- package/src/lib/components/marketing-console-page.js.map +1 -0
- package/src/lib/components/marketing-console-sections.d.ts +34 -0
- package/src/lib/components/marketing-console-sections.js +77 -0
- package/src/lib/components/marketing-console-sections.js.map +1 -0
- package/src/lib/components/overlay-stats-row.component.d.ts +15 -0
- package/src/lib/components/overlay-stats-row.component.js +78 -0
- package/src/lib/components/overlay-stats-row.component.js.map +1 -0
- package/src/lib/components/popup-card.component.d.ts +18 -0
- package/src/lib/components/popup-card.component.js +462 -0
- package/src/lib/components/popup-card.component.js.map +1 -0
- package/src/lib/components/record-attribution-widget.d.ts +35 -0
- package/src/lib/components/record-attribution-widget.js +33 -0
- package/src/lib/components/record-attribution-widget.js.map +1 -0
- package/src/lib/components/site-runtime.d.ts +9 -0
- package/src/lib/components/site-runtime.js +1081 -0
- package/src/lib/components/site-runtime.js.map +1 -0
- package/src/lib/components/use-campaign-send-api.d.ts +35 -0
- package/src/lib/components/use-campaign-send-api.js +99 -0
- package/src/lib/components/use-campaign-send-api.js.map +1 -0
- package/src/lib/components/use-campaign-topic-options.d.ts +20 -0
- package/src/lib/components/use-campaign-topic-options.js +44 -0
- package/src/lib/components/use-campaign-topic-options.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +39 -0
- package/src/lib/components/use-emails-hub-path.js +70 -0
- package/src/lib/components/use-emails-hub-path.js.map +1 -0
- package/src/lib/components/use-marketing-hub-path.d.ts +21 -0
- package/src/lib/components/use-marketing-hub-path.js +50 -0
- package/src/lib/components/use-marketing-hub-path.js.map +1 -0
- package/src/lib/components/use-overlay-copy-editor.d.ts +79 -0
- package/src/lib/components/use-overlay-copy-editor.js +110 -0
- package/src/lib/components/use-overlay-copy-editor.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/campaign-membership-figures.d.ts +109 -0
- package/src/lib/model/campaign-membership-figures.js +113 -0
- package/src/lib/model/campaign-membership-figures.js.map +1 -0
- package/src/lib/model/compile-client-automations.d.ts +65 -0
- package/src/lib/model/compile-client-automations.js +95 -0
- package/src/lib/model/compile-client-automations.js.map +1 -0
- package/src/lib/model/experiments.d.ts +130 -0
- package/src/lib/model/experiments.js +186 -0
- package/src/lib/model/experiments.js.map +1 -0
- package/src/lib/model/index.d.ts +26 -0
- package/src/lib/model/index.js +26 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketing-record-routes.d.ts +6 -0
- package/src/lib/model/marketing-record-routes.js +46 -0
- package/src/lib/model/marketing-record-routes.js.map +1 -0
- package/src/lib/model/overlay-copy.d.ts +62 -0
- package/src/lib/model/overlay-copy.js +78 -0
- package/src/lib/model/overlay-copy.js.map +1 -0
- package/src/lib/model/overlays.d.ts +113 -0
- package/src/lib/model/overlays.js +93 -0
- package/src/lib/model/overlays.js.map +1 -0
- package/src/lib/model/site-contract.d.ts +100 -0
- package/src/lib/model/site-contract.js +23 -0
- package/src/lib/model/site-contract.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +168 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/preview-props.d.ts +31 -0
- package/src/lib/preview-props.js +54 -0
- package/src/lib/preview-props.js.map +1 -0
- package/src/lib/server/campaign-manage.d.ts +62 -0
- package/src/lib/server/campaign-manage.js +829 -0
- package/src/lib/server/campaign-manage.js.map +1 -0
- package/src/lib/server/campaign-process-scheduled.d.ts +26 -0
- package/src/lib/server/campaign-process-scheduled.js +198 -0
- package/src/lib/server/campaign-process-scheduled.js.map +1 -0
- package/src/lib/server/campaign-recipients.d.ts +55 -0
- package/src/lib/server/campaign-recipients.js +219 -0
- package/src/lib/server/campaign-recipients.js.map +1 -0
- package/src/lib/server/campaign-send.d.ts +487 -0
- package/src/lib/server/campaign-send.js +3157 -0
- package/src/lib/server/campaign-send.js.map +1 -0
- package/src/lib/server/email-campaign-reach.d.ts +174 -0
- package/src/lib/server/email-campaign-reach.js +254 -0
- package/src/lib/server/email-campaign-reach.js.map +1 -0
- package/src/lib/server/email-events.d.ts +27 -0
- package/src/lib/server/email-events.js +714 -0
- package/src/lib/server/email-events.js.map +1 -0
- package/src/lib/server/get-client-automations.d.ts +60 -0
- package/src/lib/server/get-client-automations.js +87 -0
- package/src/lib/server/get-client-automations.js.map +1 -0
- package/src/lib/server/get-overlays.d.ts +28 -0
- package/src/lib/server/get-overlays.js +65 -0
- package/src/lib/server/get-overlays.js.map +1 -0
- package/src/lib/server/get-screen-experiments.d.ts +42 -0
- package/src/lib/server/get-screen-experiments.js +82 -0
- package/src/lib/server/get-screen-experiments.js.map +1 -0
- package/src/lib/server/lists-materialize.d.ts +18 -0
- package/src/lib/server/lists-materialize.js +130 -0
- package/src/lib/server/lists-materialize.js.map +1 -0
- package/src/lib/server/marketing-figures.d.ts +37 -0
- package/src/lib/server/marketing-figures.js +248 -0
- package/src/lib/server/marketing-figures.js.map +1 -0
- package/src/lib/server/site-page-enricher.d.ts +26 -0
- package/src/lib/server/site-page-enricher.js +210 -0
- package/src/lib/server/site-page-enricher.js.map +1 -0
- package/src/lib/server.d.ts +20 -0
- package/src/lib/server.js +152 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +22 -0
- package/src/lib/site.js +48 -0
- package/src/lib/site.js.map +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type HostTokenSource } from '@aglyn/aglyn';
|
|
2
|
+
export interface OverlayCopyEditor {
|
|
3
|
+
/**
|
|
4
|
+
* Stored copy as the author reads it in the text field: a variable's
|
|
5
|
+
* `{{var:id}}` shows as the `{{name}}` they typed.
|
|
6
|
+
*/
|
|
7
|
+
editable: (text: string | undefined) => string;
|
|
8
|
+
/**
|
|
9
|
+
* Typed copy as it is stored: each `{{name}}` of a variable on this site
|
|
10
|
+
* becomes its rename-safe `{{var:id}}`, the only variable form a published
|
|
11
|
+
* page resolves. A name no variable carries is left as typed.
|
|
12
|
+
*/
|
|
13
|
+
stored: (text: string | undefined) => string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* {@link stored}, capped at `max` characters as the author reads them — a
|
|
16
|
+
* cap applied to the stored form could cut an id token in half, and half a
|
|
17
|
+
* token reaches the visitor as typed.
|
|
18
|
+
*/
|
|
19
|
+
storedWithin: (text: string | undefined, max: number) => string;
|
|
20
|
+
/** What a visitor reads for this copy on the published site. */
|
|
21
|
+
preview: (text: string | undefined) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Tokens in this copy that a visitor would read exactly as typed. Empty
|
|
24
|
+
* until the variables have been read, because until then a name the site
|
|
25
|
+
* does have would be reported too.
|
|
26
|
+
*/
|
|
27
|
+
shownAsTyped: (text: string | undefined) => string[];
|
|
28
|
+
/**
|
|
29
|
+
* Whether any of this copy holds a token while the site's variables are
|
|
30
|
+
* still being read. Stored now, a name the site does have would be saved in
|
|
31
|
+
* the form no published page resolves.
|
|
32
|
+
*/
|
|
33
|
+
pending: (...texts: Array<string | undefined>) => boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Overlay copy — an announcement bar's text, a popup's headline and body —
|
|
37
|
+
* edited against the site's variables (AGL-2885).
|
|
38
|
+
*
|
|
39
|
+
* A published page resolves a variable only in its `{{var:id}}` form, while
|
|
40
|
+
* an author types `{{saleEndsAt}}`. So a name is rewritten to its id token on
|
|
41
|
+
* the way into storage and back on the way into the text field, the same
|
|
42
|
+
* normalization the Besigner applies when an element saves, and a token that
|
|
43
|
+
* would still reach a visitor as typed is reported rather than saved in
|
|
44
|
+
* silence.
|
|
45
|
+
*
|
|
46
|
+
* Callers hold copy in its STORED form and show {@link OverlayCopyEditor.editable}
|
|
47
|
+
* of it, converting each change with {@link OverlayCopyEditor.stored}. A
|
|
48
|
+
* field seeded before the variables arrive then catches up on its own, with no
|
|
49
|
+
* second seed to race the author's typing.
|
|
50
|
+
*
|
|
51
|
+
* The variables are read only once some copy holds a token, since most bars
|
|
52
|
+
* and popups are plain text, and stay read after that, so deleting a token and
|
|
53
|
+
* typing it again does not read them twice.
|
|
54
|
+
*/
|
|
55
|
+
export declare function useOverlayCopyEditor(options: {
|
|
56
|
+
hostId: string;
|
|
57
|
+
/**
|
|
58
|
+
* The site document, for the details `preview` fills in. Optional for an
|
|
59
|
+
* editor with no preview: a `{{host.*}}` token always renders as the detail
|
|
60
|
+
* or as nothing, so whether a token shows as typed never depends on it.
|
|
61
|
+
*/
|
|
62
|
+
host?: HostTokenSource | null;
|
|
63
|
+
/** Whether any copy this editor holds contains a `{{` token. */
|
|
64
|
+
usesTokens: boolean;
|
|
65
|
+
}): OverlayCopyEditor;
|
|
66
|
+
/**
|
|
67
|
+
* What a copy field says about variables while nothing in it is wrong.
|
|
68
|
+
*
|
|
69
|
+
* A placeholder rather than an example name: an example names a variable most
|
|
70
|
+
* sites do not have, and a token for a variable the site does not have is
|
|
71
|
+
* exactly what reaches a visitor as typed.
|
|
72
|
+
*/
|
|
73
|
+
export declare const OVERLAY_COPY_HELPER_TEXT = "To show a variable from Logic, type its name in double braces, like {{name}}";
|
|
74
|
+
/**
|
|
75
|
+
* The helper text a copy field shows when some of its tokens would reach a
|
|
76
|
+
* visitor as typed, or undefined when none would.
|
|
77
|
+
*/
|
|
78
|
+
export declare function shownAsTypedHelperText(tokens: string[]): string | undefined;
|
|
79
|
+
export default useOverlayCopyEditor;
|
|
@@ -0,0 +1,110 @@
|
|
|
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 { editableBindingTokens, normalizeBindingTokens } from "@aglyn/aglyn";
|
|
18
|
+
import { collection, limit, query } from "firebase/firestore";
|
|
19
|
+
import { useMemo, useState } from "react";
|
|
20
|
+
import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
|
|
21
|
+
import { overlayCopyTokensShownAsTyped, resolveOverlayCopy } from "../model/overlay-copy.js";
|
|
22
|
+
/**
|
|
23
|
+
* How many variables an editor reads: the ceiling the published page reads
|
|
24
|
+
* them under, so the editor never recognizes a name the page cannot fill in.
|
|
25
|
+
*/ const VARIABLES_CEILING = 100;
|
|
26
|
+
/**
|
|
27
|
+
* Overlay copy — an announcement bar's text, a popup's headline and body —
|
|
28
|
+
* edited against the site's variables (AGL-2885).
|
|
29
|
+
*
|
|
30
|
+
* A published page resolves a variable only in its `{{var:id}}` form, while
|
|
31
|
+
* an author types `{{saleEndsAt}}`. So a name is rewritten to its id token on
|
|
32
|
+
* the way into storage and back on the way into the text field, the same
|
|
33
|
+
* normalization the Besigner applies when an element saves, and a token that
|
|
34
|
+
* would still reach a visitor as typed is reported rather than saved in
|
|
35
|
+
* silence.
|
|
36
|
+
*
|
|
37
|
+
* Callers hold copy in its STORED form and show {@link OverlayCopyEditor.editable}
|
|
38
|
+
* of it, converting each change with {@link OverlayCopyEditor.stored}. A
|
|
39
|
+
* field seeded before the variables arrive then catches up on its own, with no
|
|
40
|
+
* second seed to race the author's typing.
|
|
41
|
+
*
|
|
42
|
+
* The variables are read only once some copy holds a token, since most bars
|
|
43
|
+
* and popups are plain text, and stay read after that, so deleting a token and
|
|
44
|
+
* typing it again does not read them twice.
|
|
45
|
+
*/ export function useOverlayCopyEditor(options) {
|
|
46
|
+
const { hostId, host, usesTokens } = options;
|
|
47
|
+
const firestore = useFirestore();
|
|
48
|
+
// Latched during render, React's pattern for state derived from an earlier
|
|
49
|
+
// render: once any copy has held a token, the read stays open.
|
|
50
|
+
const [wanted, setWanted] = useState(usesTokens);
|
|
51
|
+
if (usesTokens && !wanted) setWanted(true);
|
|
52
|
+
const { data: variableDocs, status } = useFirestoreCollection(()=>wanted ? query(collection(firestore, 'hosts', hostId, 'variables'), limit(VARIABLES_CEILING)) : null, [
|
|
53
|
+
firestore,
|
|
54
|
+
hostId,
|
|
55
|
+
wanted
|
|
56
|
+
], {
|
|
57
|
+
idField: '$id'
|
|
58
|
+
});
|
|
59
|
+
const settled = wanted && status !== 'loading';
|
|
60
|
+
return useMemo(()=>{
|
|
61
|
+
// Keyed by id for resolution, as the published page keys them, and by
|
|
62
|
+
// name for the typed form. A deleted variable is neither: the page does
|
|
63
|
+
// not read one.
|
|
64
|
+
const byId = {};
|
|
65
|
+
const byName = {};
|
|
66
|
+
for (const variable of variableDocs != null ? variableDocs : []){
|
|
67
|
+
if (!(variable == null ? void 0 : variable.name) || variable.deletedAt) continue;
|
|
68
|
+
byId[variable.$id] = variable;
|
|
69
|
+
byName[variable.name] = variable;
|
|
70
|
+
}
|
|
71
|
+
const editable = (text)=>typeof text === 'string' ? editableBindingTokens(text, byName) : '';
|
|
72
|
+
const stored = (text)=>typeof text === 'string' && text.includes('{{') ? normalizeBindingTokens(text, byName) : text;
|
|
73
|
+
return {
|
|
74
|
+
editable,
|
|
75
|
+
stored,
|
|
76
|
+
storedWithin: (text, max)=>{
|
|
77
|
+
var _stored;
|
|
78
|
+
return (_stored = stored(editable(text).slice(0, max))) != null ? _stored : '';
|
|
79
|
+
},
|
|
80
|
+
preview: (text)=>{
|
|
81
|
+
var _stored;
|
|
82
|
+
return resolveOverlayCopy((_stored = stored(text)) != null ? _stored : '', byId, host);
|
|
83
|
+
},
|
|
84
|
+
shownAsTyped: (text)=>settled ? overlayCopyTokensShownAsTyped(stored(text), byId, host) : [],
|
|
85
|
+
pending: (...texts)=>!settled && texts.some((text)=>Boolean(text == null ? void 0 : text.includes('{{')))
|
|
86
|
+
};
|
|
87
|
+
}, [
|
|
88
|
+
variableDocs,
|
|
89
|
+
settled,
|
|
90
|
+
host
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* What a copy field says about variables while nothing in it is wrong.
|
|
95
|
+
*
|
|
96
|
+
* A placeholder rather than an example name: an example names a variable most
|
|
97
|
+
* sites do not have, and a token for a variable the site does not have is
|
|
98
|
+
* exactly what reaches a visitor as typed.
|
|
99
|
+
*/ export const OVERLAY_COPY_HELPER_TEXT = 'To show a variable from Logic, type its name in double braces, like {{name}}';
|
|
100
|
+
/**
|
|
101
|
+
* The helper text a copy field shows when some of its tokens would reach a
|
|
102
|
+
* visitor as typed, or undefined when none would.
|
|
103
|
+
*/ export function shownAsTypedHelperText(tokens) {
|
|
104
|
+
if (!tokens.length) return undefined;
|
|
105
|
+
const list = tokens.join(', ');
|
|
106
|
+
return `Visitors would see ${list} exactly as typed. Use the name of a ` + 'variable from Logic, or remove the braces.';
|
|
107
|
+
}
|
|
108
|
+
export default useOverlayCopyEditor;
|
|
109
|
+
|
|
110
|
+
//# sourceMappingURL=use-overlay-copy-editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/use-overlay-copy-editor.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'use client'\n\nimport {\n type BindingDocRef,\n editableBindingTokens,\n type HostTokenSource,\n type HostVariable,\n normalizeBindingTokens,\n} from '@aglyn/aglyn'\nimport { collection, limit, query } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n overlayCopyTokensShownAsTyped,\n resolveOverlayCopy,\n} from '../model/overlay-copy'\n\n/**\n * How many variables an editor reads: the ceiling the published page reads\n * them under, so the editor never recognizes a name the page cannot fill in.\n */\nconst VARIABLES_CEILING = 100\n\ntype VariableDoc = HostVariable & { $id: string; deletedAt?: unknown }\n\nexport interface OverlayCopyEditor {\n /**\n * Stored copy as the author reads it in the text field: a variable's\n * `{{var:id}}` shows as the `{{name}}` they typed.\n */\n editable: (text: string | undefined) => string\n /**\n * Typed copy as it is stored: each `{{name}}` of a variable on this site\n * becomes its rename-safe `{{var:id}}`, the only variable form a published\n * page resolves. A name no variable carries is left as typed.\n */\n stored: (text: string | undefined) => string | undefined\n /**\n * {@link stored}, capped at `max` characters as the author reads them — a\n * cap applied to the stored form could cut an id token in half, and half a\n * token reaches the visitor as typed.\n */\n storedWithin: (text: string | undefined, max: number) => string\n /** What a visitor reads for this copy on the published site. */\n preview: (text: string | undefined) => string\n /**\n * Tokens in this copy that a visitor would read exactly as typed. Empty\n * until the variables have been read, because until then a name the site\n * does have would be reported too.\n */\n shownAsTyped: (text: string | undefined) => string[]\n /**\n * Whether any of this copy holds a token while the site's variables are\n * still being read. Stored now, a name the site does have would be saved in\n * the form no published page resolves.\n */\n pending: (...texts: Array<string | undefined>) => boolean\n}\n\n/**\n * Overlay copy — an announcement bar's text, a popup's headline and body —\n * edited against the site's variables (AGL-2885).\n *\n * A published page resolves a variable only in its `{{var:id}}` form, while\n * an author types `{{saleEndsAt}}`. So a name is rewritten to its id token on\n * the way into storage and back on the way into the text field, the same\n * normalization the Besigner applies when an element saves, and a token that\n * would still reach a visitor as typed is reported rather than saved in\n * silence.\n *\n * Callers hold copy in its STORED form and show {@link OverlayCopyEditor.editable}\n * of it, converting each change with {@link OverlayCopyEditor.stored}. A\n * field seeded before the variables arrive then catches up on its own, with no\n * second seed to race the author's typing.\n *\n * The variables are read only once some copy holds a token, since most bars\n * and popups are plain text, and stay read after that, so deleting a token and\n * typing it again does not read them twice.\n */\nexport function useOverlayCopyEditor(options: {\n hostId: string\n /**\n * The site document, for the details `preview` fills in. Optional for an\n * editor with no preview: a `{{host.*}}` token always renders as the detail\n * or as nothing, so whether a token shows as typed never depends on it.\n */\n host?: HostTokenSource | null\n /** Whether any copy this editor holds contains a `{{` token. */\n usesTokens: boolean\n}): OverlayCopyEditor {\n const { hostId, host, usesTokens } = options\n const firestore = useFirestore()\n // Latched during render, React's pattern for state derived from an earlier\n // render: once any copy has held a token, the read stays open.\n const [wanted, setWanted] = useState(usesTokens)\n if (usesTokens && !wanted) setWanted(true)\n const { data: variableDocs, status } = useFirestoreCollection<VariableDoc>(\n () =>\n wanted\n ? query(\n collection(firestore, 'hosts', hostId, 'variables'),\n limit(VARIABLES_CEILING),\n )\n : null,\n [firestore, hostId, wanted],\n { idField: '$id' },\n )\n const settled = wanted && status !== 'loading'\n\n return useMemo(() => {\n // Keyed by id for resolution, as the published page keys them, and by\n // name for the typed form. A deleted variable is neither: the page does\n // not read one.\n const byId: Record<string, HostVariable> = {}\n const byName: Record<string, BindingDocRef> = {}\n for (const variable of variableDocs ?? []) {\n if (!variable?.name || variable.deletedAt) continue\n byId[variable.$id] = variable\n byName[variable.name] = variable\n }\n const editable = (text: string | undefined) =>\n typeof text === 'string' ? editableBindingTokens(text, byName) : ''\n const stored = (text: string | undefined) =>\n typeof text === 'string' && text.includes('{{')\n ? normalizeBindingTokens(text, byName)\n : text\n return {\n editable,\n stored,\n storedWithin: (text, max) => stored(editable(text).slice(0, max)) ?? '',\n preview: (text) => resolveOverlayCopy(stored(text) ?? '', byId, host),\n shownAsTyped: (text) =>\n settled ? overlayCopyTokensShownAsTyped(stored(text), byId, host) : [],\n pending: (...texts) =>\n !settled && texts.some((text) => Boolean(text?.includes('{{'))),\n }\n }, [variableDocs, settled, host])\n}\n\n/**\n * What a copy field says about variables while nothing in it is wrong.\n *\n * A placeholder rather than an example name: an example names a variable most\n * sites do not have, and a token for a variable the site does not have is\n * exactly what reaches a visitor as typed.\n */\nexport const OVERLAY_COPY_HELPER_TEXT =\n 'To show a variable from Logic, type its name in double braces, like {{name}}'\n\n/**\n * The helper text a copy field shows when some of its tokens would reach a\n * visitor as typed, or undefined when none would.\n */\nexport function shownAsTypedHelperText(tokens: string[]): string | undefined {\n if (!tokens.length) return undefined\n const list = tokens.join(', ')\n return (\n `Visitors would see ${list} exactly as typed. Use the name of a ` +\n 'variable from Logic, or remove the braces.'\n )\n}\n\nexport default useOverlayCopyEditor\n"],"names":["editableBindingTokens","normalizeBindingTokens","collection","limit","query","useMemo","useState","useFirestore","useFirestoreCollection","overlayCopyTokensShownAsTyped","resolveOverlayCopy","VARIABLES_CEILING","useOverlayCopyEditor","options","hostId","host","usesTokens","firestore","wanted","setWanted","data","variableDocs","status","idField","settled","byId","byName","variable","name","deletedAt","$id","editable","text","stored","includes","storedWithin","max","slice","preview","shownAsTyped","pending","texts","some","Boolean","OVERLAY_COPY_HELPER_TEXT","shownAsTypedHelperText","tokens","length","undefined","list","join"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAEEA,qBAAqB,EAGrBC,sBAAsB,QACjB,eAAc;AACrB,SAASC,UAAU,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AAC7D,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACzC,SACEC,YAAY,EACZC,sBAAsB,QACjB,iCAAgC;AACvC,SACEC,6BAA6B,EAC7BC,kBAAkB,QACb,2BAAuB;AAE9B;;;CAGC,GACD,MAAMC,oBAAoB;AAsC1B;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,qBAAqBC,OAUpC;IACC,MAAM,EAAEC,MAAM,EAAEC,IAAI,EAAEC,UAAU,EAAE,GAAGH;IACrC,MAAMI,YAAYV;IAClB,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAM,CAACW,QAAQC,UAAU,GAAGb,SAASU;IACrC,IAAIA,cAAc,CAACE,QAAQC,UAAU;IACrC,MAAM,EAAEC,MAAMC,YAAY,EAAEC,MAAM,EAAE,GAAGd,uBACrC,IACEU,SACId,MACEF,WAAWe,WAAW,SAASH,QAAQ,cACvCX,MAAMQ,sBAER,MACN;QAACM;QAAWH;QAAQI;KAAO,EAC3B;QAAEK,SAAS;IAAM;IAEnB,MAAMC,UAAUN,UAAUI,WAAW;IAErC,OAAOjB,QAAQ;QACb,sEAAsE;QACtE,wEAAwE;QACxE,gBAAgB;QAChB,MAAMoB,OAAqC,CAAC;QAC5C,MAAMC,SAAwC,CAAC;QAC/C,KAAK,MAAMC,YAAYN,uBAAAA,eAAgB,EAAE,CAAE;YACzC,IAAI,EAACM,4BAAAA,SAAUC,IAAI,KAAID,SAASE,SAAS,EAAE;YAC3CJ,IAAI,CAACE,SAASG,GAAG,CAAC,GAAGH;YACrBD,MAAM,CAACC,SAASC,IAAI,CAAC,GAAGD;QAC1B;QACA,MAAMI,WAAW,CAACC,OAChB,OAAOA,SAAS,WAAWhC,sBAAsBgC,MAAMN,UAAU;QACnE,MAAMO,SAAS,CAACD,OACd,OAAOA,SAAS,YAAYA,KAAKE,QAAQ,CAAC,QACtCjC,uBAAuB+B,MAAMN,UAC7BM;QACN,OAAO;YACLD;YACAE;YACAE,cAAc,CAACH,MAAMI;oBAAQH;wBAAAA,UAAAA,OAAOF,SAASC,MAAMK,KAAK,CAAC,GAAGD,iBAA/BH,UAAwC;;YACrEK,SAAS,CAACN;oBAA4BC;uBAAnBvB,oBAAmBuB,UAAAA,OAAOD,iBAAPC,UAAgB,IAAIR,MAAMV;;YAChEwB,cAAc,CAACP,OACbR,UAAUf,8BAA8BwB,OAAOD,OAAOP,MAAMV,QAAQ,EAAE;YACxEyB,SAAS,CAAC,GAAGC,QACX,CAACjB,WAAWiB,MAAMC,IAAI,CAAC,CAACV,OAASW,QAAQX,wBAAAA,KAAME,QAAQ,CAAC;QAC5D;IACF,GAAG;QAACb;QAAcG;QAAST;KAAK;AAClC;AAEA;;;;;;CAMC,GACD,OAAO,MAAM6B,2BACX,+EAA8E;AAEhF;;;CAGC,GACD,OAAO,SAASC,uBAAuBC,MAAgB;IACrD,IAAI,CAACA,OAAOC,MAAM,EAAE,OAAOC;IAC3B,MAAMC,OAAOH,OAAOI,IAAI,CAAC;IACzB,OACE,CAAC,mBAAmB,EAAED,KAAK,qCAAqC,CAAC,GACjE;AAEJ;AAEA,eAAerC,qBAAoB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
*/ export const BUNDLE_ID = 'marketing';
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=bundle-common.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/constants/bundle-common.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 */\n\nexport const BUNDLE_ID = 'marketing'\n"],"names":["BUNDLE_ID"],"mappings":"AAAA;;;;;;CAMC,GAED,OAAO,MAAMA,YAAY,YAAW"}
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
* WHAT THE RECORDS FILED UNDER A CAMPAIGN HOLD — which is not what the
|
|
19
|
+
* campaign caused.
|
|
20
|
+
*
|
|
21
|
+
* Every other figure on a campaign's page is ATTRIBUTION: the campaign's mail
|
|
22
|
+
* carried a link, a visitor followed it, and the conversion, the money or the
|
|
23
|
+
* landing is credited to the campaign because that chain was recorded. The
|
|
24
|
+
* figures this module produces are MEMBERSHIP: somebody put a form in this
|
|
25
|
+
* campaign, and the form has counters of its own.
|
|
26
|
+
*
|
|
27
|
+
* The two answer different questions and a reader who adds them together gets
|
|
28
|
+
* a wrong number in a direction that flatters the campaign. So nothing here
|
|
29
|
+
* is named for causation — nothing generated, nothing driven, nothing from
|
|
30
|
+
* this campaign — and the rollup carries the three facts a caller needs in
|
|
31
|
+
* order to describe the figure honestly rather than merely print it:
|
|
32
|
+
*
|
|
33
|
+
* 1. **Whether it is windowed.** A form's flat counters are lifetime and
|
|
34
|
+
* include everything from before it was ever filed under this campaign.
|
|
35
|
+
* 2. **How many members recorded it.** A counter nothing recorded is `null`,
|
|
36
|
+
* and a sum over three of five forms is a different statement from a sum
|
|
37
|
+
* over five.
|
|
38
|
+
* 3. **How many members are in another campaign too.** `campaignIds` holds
|
|
39
|
+
* up to twenty ids, so the same submissions legitimately count toward
|
|
40
|
+
* more than one campaign and no total here is exclusive.
|
|
41
|
+
*/
|
|
42
|
+
import { type FormPeriodRange, type FormStatKind, type FormStats, type FormStatsTotals } from '@aglyn/aglyn/app-utils/forms';
|
|
43
|
+
/** The dates a campaign container carries, as this module reads them. */
|
|
44
|
+
export interface CampaignSpan {
|
|
45
|
+
startAtMs?: number | null;
|
|
46
|
+
endAtMs?: number | null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The months a campaign's dates cover, for {@link formStatsTotals}.
|
|
50
|
+
*
|
|
51
|
+
* Both ends are optional in the stored campaign and stay optional here: an
|
|
52
|
+
* open-ended campaign is genuinely open-ended, and substituting today for a
|
|
53
|
+
* missing end would silently exclude a month whose figures are still moving.
|
|
54
|
+
*
|
|
55
|
+
* A campaign with NEITHER date produces an empty range, which
|
|
56
|
+
* {@link formStatsTotals} answers with lifetime totals. That is the correct
|
|
57
|
+
* answer — there is no window to confine the figures to — and it is why the
|
|
58
|
+
* caller must go on saying which of the two it is showing.
|
|
59
|
+
*/
|
|
60
|
+
export declare function campaignPeriodRange(span: CampaignSpan): FormPeriodRange;
|
|
61
|
+
/** Whether a range confines anything. */
|
|
62
|
+
export declare function isWindowedRange(range: FormPeriodRange): boolean;
|
|
63
|
+
/** One member's contribution to the rollup. */
|
|
64
|
+
export interface CampaignFormMember {
|
|
65
|
+
/** The form's own counters, already summed over the range or lifetime. */
|
|
66
|
+
totals: FormStatsTotals;
|
|
67
|
+
/** Campaigns the form is filed under, this one included. */
|
|
68
|
+
campaigns: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* One summed counter, with the population it was summed over named.
|
|
72
|
+
*
|
|
73
|
+
* The shape `CampaignAggregate` uses for the mail figures, deliberately: a
|
|
74
|
+
* reader meets both on the same page, and a total that names its population
|
|
75
|
+
* on one section and not the other teaches that the silent one is complete.
|
|
76
|
+
*/
|
|
77
|
+
export interface CampaignMemberFigure {
|
|
78
|
+
/** `null` when no member recorded this counter at all. */
|
|
79
|
+
value: number | null;
|
|
80
|
+
/** Members that recorded it. */
|
|
81
|
+
recorded: number;
|
|
82
|
+
/** Members the sum ran over. */
|
|
83
|
+
members: number;
|
|
84
|
+
}
|
|
85
|
+
/** Every membership figure for one campaign's forms. */
|
|
86
|
+
export interface CampaignFormsRollup extends Record<FormStatKind, CampaignMemberFigure> {
|
|
87
|
+
/** Forms filed under this campaign. */
|
|
88
|
+
members: number;
|
|
89
|
+
/** Of those, the ones filed under at least one other campaign as well. */
|
|
90
|
+
shared: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Add up what a campaign's forms hold.
|
|
94
|
+
*
|
|
95
|
+
* A counter stays `null` until some member records it, so a campaign whose
|
|
96
|
+
* forms have never counted views reports views as unmeasured rather than as
|
|
97
|
+
* zero. `recorded` is what lets the caller say "across 2 of 5 forms" where
|
|
98
|
+
* the two disagree — the same admission the mail rollup makes when a send is
|
|
99
|
+
* missing a counter.
|
|
100
|
+
*/
|
|
101
|
+
export declare function campaignFormsRollup(members: readonly CampaignFormMember[]): CampaignFormsRollup;
|
|
102
|
+
/**
|
|
103
|
+
* A form's counters as this campaign may report them.
|
|
104
|
+
*
|
|
105
|
+
* Thin on purpose — the windowing rule lives in `formStatsTotals` so the
|
|
106
|
+
* forms surface and this one cannot disagree about which months a figure
|
|
107
|
+
* covers.
|
|
108
|
+
*/
|
|
109
|
+
export declare function campaignFormTotals(stats: FormStats | undefined | null, range: FormPeriodRange): FormStatsTotals;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* WHAT THE RECORDS FILED UNDER A CAMPAIGN HOLD — which is not what the
|
|
19
|
+
* campaign caused.
|
|
20
|
+
*
|
|
21
|
+
* Every other figure on a campaign's page is ATTRIBUTION: the campaign's mail
|
|
22
|
+
* carried a link, a visitor followed it, and the conversion, the money or the
|
|
23
|
+
* landing is credited to the campaign because that chain was recorded. The
|
|
24
|
+
* figures this module produces are MEMBERSHIP: somebody put a form in this
|
|
25
|
+
* campaign, and the form has counters of its own.
|
|
26
|
+
*
|
|
27
|
+
* The two answer different questions and a reader who adds them together gets
|
|
28
|
+
* a wrong number in a direction that flatters the campaign. So nothing here
|
|
29
|
+
* is named for causation — nothing generated, nothing driven, nothing from
|
|
30
|
+
* this campaign — and the rollup carries the three facts a caller needs in
|
|
31
|
+
* order to describe the figure honestly rather than merely print it:
|
|
32
|
+
*
|
|
33
|
+
* 1. **Whether it is windowed.** A form's flat counters are lifetime and
|
|
34
|
+
* include everything from before it was ever filed under this campaign.
|
|
35
|
+
* 2. **How many members recorded it.** A counter nothing recorded is `null`,
|
|
36
|
+
* and a sum over three of five forms is a different statement from a sum
|
|
37
|
+
* over five.
|
|
38
|
+
* 3. **How many members are in another campaign too.** `campaignIds` holds
|
|
39
|
+
* up to twenty ids, so the same submissions legitimately count toward
|
|
40
|
+
* more than one campaign and no total here is exclusive.
|
|
41
|
+
*/ // Import the form-stat helpers from the context-free forms subpath, NOT the
|
|
42
|
+
// `@aglyn/aglyn` barrel: this file is in the marketing model, which the
|
|
43
|
+
// plugin's SERVER graph re-exports, and the full barrel pulls client React
|
|
44
|
+
// contexts (createContext) that break the RSC server build (AGL-830).
|
|
45
|
+
import { formPeriodKey, formStatsTotals, FORM_STAT_KINDS } from "@aglyn/aglyn/app-utils/forms";
|
|
46
|
+
/**
|
|
47
|
+
* The months a campaign's dates cover, for {@link formStatsTotals}.
|
|
48
|
+
*
|
|
49
|
+
* Both ends are optional in the stored campaign and stay optional here: an
|
|
50
|
+
* open-ended campaign is genuinely open-ended, and substituting today for a
|
|
51
|
+
* missing end would silently exclude a month whose figures are still moving.
|
|
52
|
+
*
|
|
53
|
+
* A campaign with NEITHER date produces an empty range, which
|
|
54
|
+
* {@link formStatsTotals} answers with lifetime totals. That is the correct
|
|
55
|
+
* answer — there is no window to confine the figures to — and it is why the
|
|
56
|
+
* caller must go on saying which of the two it is showing.
|
|
57
|
+
*/ export function campaignPeriodRange(span) {
|
|
58
|
+
const from = formPeriodKey(span.startAtMs);
|
|
59
|
+
const to = formPeriodKey(span.endAtMs);
|
|
60
|
+
return _extends({}, from ? {
|
|
61
|
+
from
|
|
62
|
+
} : {}, to ? {
|
|
63
|
+
to
|
|
64
|
+
} : {});
|
|
65
|
+
}
|
|
66
|
+
/** Whether a range confines anything. */ export function isWindowedRange(range) {
|
|
67
|
+
return Boolean(range.from || range.to);
|
|
68
|
+
}
|
|
69
|
+
/** A figure nothing has been added to yet. */ function emptyFigure(members) {
|
|
70
|
+
return {
|
|
71
|
+
value: null,
|
|
72
|
+
recorded: 0,
|
|
73
|
+
members
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Add up what a campaign's forms hold.
|
|
78
|
+
*
|
|
79
|
+
* A counter stays `null` until some member records it, so a campaign whose
|
|
80
|
+
* forms have never counted views reports views as unmeasured rather than as
|
|
81
|
+
* zero. `recorded` is what lets the caller say "across 2 of 5 forms" where
|
|
82
|
+
* the two disagree — the same admission the mail rollup makes when a send is
|
|
83
|
+
* missing a counter.
|
|
84
|
+
*/ export function campaignFormsRollup(members) {
|
|
85
|
+
const rollup = {
|
|
86
|
+
members: members.length,
|
|
87
|
+
shared: 0
|
|
88
|
+
};
|
|
89
|
+
for (const kind of FORM_STAT_KINDS)rollup[kind] = emptyFigure(members.length);
|
|
90
|
+
for (const member of members){
|
|
91
|
+
if (member.campaigns > 1) rollup.shared += 1;
|
|
92
|
+
for (const kind of FORM_STAT_KINDS){
|
|
93
|
+
var _figure_value;
|
|
94
|
+
const value = member.totals[kind];
|
|
95
|
+
if (value === null) continue;
|
|
96
|
+
const figure = rollup[kind];
|
|
97
|
+
figure.value = ((_figure_value = figure.value) != null ? _figure_value : 0) + value;
|
|
98
|
+
figure.recorded += 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return rollup;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A form's counters as this campaign may report them.
|
|
105
|
+
*
|
|
106
|
+
* Thin on purpose — the windowing rule lives in `formStatsTotals` so the
|
|
107
|
+
* forms surface and this one cannot disagree about which months a figure
|
|
108
|
+
* covers.
|
|
109
|
+
*/ export function campaignFormTotals(stats, range) {
|
|
110
|
+
return formStatsTotals(stats, isWindowedRange(range) ? range : null);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=campaign-membership-figures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/campaign-membership-figures.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\n/**\n * WHAT THE RECORDS FILED UNDER A CAMPAIGN HOLD — which is not what the\n * campaign caused.\n *\n * Every other figure on a campaign's page is ATTRIBUTION: the campaign's mail\n * carried a link, a visitor followed it, and the conversion, the money or the\n * landing is credited to the campaign because that chain was recorded. The\n * figures this module produces are MEMBERSHIP: somebody put a form in this\n * campaign, and the form has counters of its own.\n *\n * The two answer different questions and a reader who adds them together gets\n * a wrong number in a direction that flatters the campaign. So nothing here\n * is named for causation — nothing generated, nothing driven, nothing from\n * this campaign — and the rollup carries the three facts a caller needs in\n * order to describe the figure honestly rather than merely print it:\n *\n * 1. **Whether it is windowed.** A form's flat counters are lifetime and\n * include everything from before it was ever filed under this campaign.\n * 2. **How many members recorded it.** A counter nothing recorded is `null`,\n * and a sum over three of five forms is a different statement from a sum\n * over five.\n * 3. **How many members are in another campaign too.** `campaignIds` holds\n * up to twenty ids, so the same submissions legitimately count toward\n * more than one campaign and no total here is exclusive.\n */\n\n// Import the form-stat helpers from the context-free forms subpath, NOT the\n// `@aglyn/aglyn` barrel: this file is in the marketing model, which the\n// plugin's SERVER graph re-exports, and the full barrel pulls client React\n// contexts (createContext) that break the RSC server build (AGL-830).\nimport {\n formPeriodKey,\n formStatsTotals,\n FORM_STAT_KINDS,\n type FormPeriodRange,\n type FormStatKind,\n type FormStats,\n type FormStatsTotals,\n} from '@aglyn/aglyn/app-utils/forms'\n\n/** The dates a campaign container carries, as this module reads them. */\nexport interface CampaignSpan {\n startAtMs?: number | null\n endAtMs?: number | null\n}\n\n/**\n * The months a campaign's dates cover, for {@link formStatsTotals}.\n *\n * Both ends are optional in the stored campaign and stay optional here: an\n * open-ended campaign is genuinely open-ended, and substituting today for a\n * missing end would silently exclude a month whose figures are still moving.\n *\n * A campaign with NEITHER date produces an empty range, which\n * {@link formStatsTotals} answers with lifetime totals. That is the correct\n * answer — there is no window to confine the figures to — and it is why the\n * caller must go on saying which of the two it is showing.\n */\nexport function campaignPeriodRange(span: CampaignSpan): FormPeriodRange {\n const from = formPeriodKey(span.startAtMs)\n const to = formPeriodKey(span.endAtMs)\n return { ...(from ? { from } : {}), ...(to ? { to } : {}) }\n}\n\n/** Whether a range confines anything. */\nexport function isWindowedRange(range: FormPeriodRange): boolean {\n return Boolean(range.from || range.to)\n}\n\n/** One member's contribution to the rollup. */\nexport interface CampaignFormMember {\n /** The form's own counters, already summed over the range or lifetime. */\n totals: FormStatsTotals\n /** Campaigns the form is filed under, this one included. */\n campaigns: number\n}\n\n/**\n * One summed counter, with the population it was summed over named.\n *\n * The shape `CampaignAggregate` uses for the mail figures, deliberately: a\n * reader meets both on the same page, and a total that names its population\n * on one section and not the other teaches that the silent one is complete.\n */\nexport interface CampaignMemberFigure {\n /** `null` when no member recorded this counter at all. */\n value: number | null\n /** Members that recorded it. */\n recorded: number\n /** Members the sum ran over. */\n members: number\n}\n\n/** Every membership figure for one campaign's forms. */\nexport interface CampaignFormsRollup extends Record<FormStatKind, CampaignMemberFigure> {\n /** Forms filed under this campaign. */\n members: number\n /** Of those, the ones filed under at least one other campaign as well. */\n shared: number\n}\n\n/** A figure nothing has been added to yet. */\nfunction emptyFigure(members: number): CampaignMemberFigure {\n return { value: null, recorded: 0, members }\n}\n\n/**\n * Add up what a campaign's forms hold.\n *\n * A counter stays `null` until some member records it, so a campaign whose\n * forms have never counted views reports views as unmeasured rather than as\n * zero. `recorded` is what lets the caller say \"across 2 of 5 forms\" where\n * the two disagree — the same admission the mail rollup makes when a send is\n * missing a counter.\n */\nexport function campaignFormsRollup(\n members: readonly CampaignFormMember[],\n): CampaignFormsRollup {\n const rollup = {\n members: members.length,\n shared: 0,\n } as CampaignFormsRollup\n for (const kind of FORM_STAT_KINDS) rollup[kind] = emptyFigure(members.length)\n for (const member of members) {\n if (member.campaigns > 1) rollup.shared += 1\n for (const kind of FORM_STAT_KINDS) {\n const value = member.totals[kind]\n if (value === null) continue\n const figure = rollup[kind]\n figure.value = (figure.value ?? 0) + value\n figure.recorded += 1\n }\n }\n return rollup\n}\n\n/**\n * A form's counters as this campaign may report them.\n *\n * Thin on purpose — the windowing rule lives in `formStatsTotals` so the\n * forms surface and this one cannot disagree about which months a figure\n * covers.\n */\nexport function campaignFormTotals(\n stats: FormStats | undefined | null,\n range: FormPeriodRange,\n): FormStatsTotals {\n return formStatsTotals(stats, isWindowedRange(range) ? range : null)\n}\n"],"names":["formPeriodKey","formStatsTotals","FORM_STAT_KINDS","campaignPeriodRange","span","from","startAtMs","to","endAtMs","isWindowedRange","range","Boolean","emptyFigure","members","value","recorded","campaignFormsRollup","rollup","length","shared","kind","member","campaigns","figure","totals","campaignFormTotals","stats"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GAED,4EAA4E;AAC5E,wEAAwE;AACxE,2EAA2E;AAC3E,sEAAsE;AACtE,SACEA,aAAa,EACbC,eAAe,EACfC,eAAe,QAKV,+BAA8B;AAQrC;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,oBAAoBC,IAAkB;IACpD,MAAMC,OAAOL,cAAcI,KAAKE,SAAS;IACzC,MAAMC,KAAKP,cAAcI,KAAKI,OAAO;IACrC,OAAO,aAAMH,OAAO;QAAEA;IAAK,IAAI,CAAC,GAAQE,KAAK;QAAEA;IAAG,IAAI,CAAC;AACzD;AAEA,uCAAuC,GACvC,OAAO,SAASE,gBAAgBC,KAAsB;IACpD,OAAOC,QAAQD,MAAML,IAAI,IAAIK,MAAMH,EAAE;AACvC;AAkCA,4CAA4C,GAC5C,SAASK,YAAYC,OAAe;IAClC,OAAO;QAAEC,OAAO;QAAMC,UAAU;QAAGF;IAAQ;AAC7C;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASG,oBACdH,OAAsC;IAEtC,MAAMI,SAAS;QACbJ,SAASA,QAAQK,MAAM;QACvBC,QAAQ;IACV;IACA,KAAK,MAAMC,QAAQlB,gBAAiBe,MAAM,CAACG,KAAK,GAAGR,YAAYC,QAAQK,MAAM;IAC7E,KAAK,MAAMG,UAAUR,QAAS;QAC5B,IAAIQ,OAAOC,SAAS,GAAG,GAAGL,OAAOE,MAAM,IAAI;QAC3C,KAAK,MAAMC,QAAQlB,gBAAiB;gBAIlBqB;YAHhB,MAAMT,QAAQO,OAAOG,MAAM,CAACJ,KAAK;YACjC,IAAIN,UAAU,MAAM;YACpB,MAAMS,SAASN,MAAM,CAACG,KAAK;YAC3BG,OAAOT,KAAK,GAAG,EAACS,gBAAAA,OAAOT,KAAK,YAAZS,gBAAgB,KAAKT;YACrCS,OAAOR,QAAQ,IAAI;QACrB;IACF;IACA,OAAOE;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASQ,mBACdC,KAAmC,EACnChB,KAAsB;IAEtB,OAAOT,gBAAgByB,OAAOjB,gBAAgBC,SAASA,QAAQ;AACjE"}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
import { type HostAction } from '@aglyn/aglyn/app-utils/actions';
|
|
18
|
+
import type { ClientAutomation } from './site-contract';
|
|
19
|
+
/** A raw `hosts/{hostId}/actions` doc paired with its id. */
|
|
20
|
+
export interface RawHostAction {
|
|
21
|
+
id: string;
|
|
22
|
+
action: HostAction;
|
|
23
|
+
}
|
|
24
|
+
export interface CompileClientAutomationsOptions {
|
|
25
|
+
/** Leading-slash page path for pathPattern targeting. */
|
|
26
|
+
path: string;
|
|
27
|
+
/**
|
|
28
|
+
* `actions` entitlement (AGL-577). Basic presentational steps
|
|
29
|
+
* (menu/drawer/show-hide/class/nav/alert) always load; advanced client
|
|
30
|
+
* steps (overlay/showHtml/analytics) and server steps drop when false.
|
|
31
|
+
*/
|
|
32
|
+
actionsEntitled: boolean;
|
|
33
|
+
/** Business-tier gate for `runJs` steps (dropped when false). */
|
|
34
|
+
allowJs: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Ignore each action's `pathPattern` (AGL-830). The besigner Preview shows
|
|
37
|
+
* a single composed screen and scopes relevance by selector match, so it
|
|
38
|
+
* skips path filtering rather than resolve the composed path.
|
|
39
|
+
*/
|
|
40
|
+
matchAllPaths?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Drop every automation that names a `pathPattern` (AGL-2511), for a
|
|
43
|
+
* surface whose own path is unknown — the designed 404 body, served from a
|
|
44
|
+
* per-host cache that no request path belongs to.
|
|
45
|
+
*
|
|
46
|
+
* The opposite of `matchAllPaths` and deliberately so: Preview knows the
|
|
47
|
+
* page and only lacks its composed address, so it keeps path-scoped
|
|
48
|
+
* automations; this caller cannot know the page at all, and running an
|
|
49
|
+
* automation scoped to `/pricing` on whatever URL 404'd would be a guess
|
|
50
|
+
* with a side effect. Interactions authored on nodes carry no pattern, so
|
|
51
|
+
* a nav's hover choreography — the reason this surface is enriched — is
|
|
52
|
+
* unaffected.
|
|
53
|
+
*/
|
|
54
|
+
dropPathScoped?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pure action→client-automation mapping (AGL-256/577, extracted AGL-830):
|
|
58
|
+
* the single place the trigger config + entitlement-trimmed client steps are
|
|
59
|
+
* shaped. Both the server page-enricher (admin fetch) and the editor Preview
|
|
60
|
+
* (client fetch) feed their loaded action docs through this, so live and
|
|
61
|
+
* preview stay byte-for-byte consistent. No I/O, no firebase — safe on the
|
|
62
|
+
* client. Malformed entries are skipped, never thrown.
|
|
63
|
+
*/
|
|
64
|
+
export declare function compileClientAutomations(actions: RawHostAction[], options: CompileClientAutomationsOptions): ClientAutomation[];
|
|
65
|
+
export default compileClientAutomations;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ // Import the entitlement/event predicates from the context-free actions
|
|
18
|
+
// subpath, NOT the `@aglyn/aglyn` barrel: this file is in the marketing model,
|
|
19
|
+
// which the plugin's SERVER graph re-exports, and the full barrel pulls client
|
|
20
|
+
// React contexts (createContext) that break the RSC server build (AGL-830).
|
|
21
|
+
import { hostActionStepsForClient, isClientActionStep, isClientStepEntitled, isSiteEventType } from "@aglyn/aglyn/app-utils/actions";
|
|
22
|
+
import { overlayMatchesPath } from "./overlays.js";
|
|
23
|
+
/**
|
|
24
|
+
* Pure action→client-automation mapping (AGL-256/577, extracted AGL-830):
|
|
25
|
+
* the single place the trigger config + entitlement-trimmed client steps are
|
|
26
|
+
* shaped. Both the server page-enricher (admin fetch) and the editor Preview
|
|
27
|
+
* (client fetch) feed their loaded action docs through this, so live and
|
|
28
|
+
* preview stay byte-for-byte consistent. No I/O, no firebase — safe on the
|
|
29
|
+
* client. Malformed entries are skipped, never thrown.
|
|
30
|
+
*/ export function compileClientAutomations(actions, options) {
|
|
31
|
+
const automations = [];
|
|
32
|
+
for (const { id, action } of actions){
|
|
33
|
+
var _ref, _action_steps;
|
|
34
|
+
var _action_trigger, _action_trigger_pathPattern, _action_trigger1, _action_trigger2, _action_trigger3, _action_trigger4, _action_trigger5, _action_trigger6, _action_trigger7, _action_trigger8;
|
|
35
|
+
if (action.deletedAt || action.enabled === false) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const event = String((_ref = (_action_trigger = action.trigger) == null ? void 0 : _action_trigger.event) != null ? _ref : '');
|
|
39
|
+
if (!isSiteEventType(event)) continue;
|
|
40
|
+
const pathPattern = (_action_trigger1 = action.trigger) == null ? void 0 : (_action_trigger_pathPattern = _action_trigger1.pathPattern) == null ? void 0 : _action_trigger_pathPattern.trim();
|
|
41
|
+
if (pathPattern && options.dropPathScoped) continue;
|
|
42
|
+
if (!options.matchAllPaths && pathPattern && !overlayMatchesPath({
|
|
43
|
+
pathPatterns: [
|
|
44
|
+
pathPattern
|
|
45
|
+
]
|
|
46
|
+
}, options.path)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
/*
|
|
50
|
+
* TRUNCATED AT THE FIRST WAIT, before anything else is decided.
|
|
51
|
+
*
|
|
52
|
+
* The browser runs its slice of the list the moment the trigger fires, so
|
|
53
|
+
* a client step sitting after a `wait` would execute at once — the popup
|
|
54
|
+
* an author scheduled for three days later would appear immediately, the
|
|
55
|
+
* delay would look correct on the server, and the two halves of one
|
|
56
|
+
* authored flow would disagree about when they happened. Everything past
|
|
57
|
+
* the first wait belongs to a run the job beat has not started yet.
|
|
58
|
+
*/ const reachableSteps = hostActionStepsForClient(action.steps);
|
|
59
|
+
// Step-level entitlement tiering (AGL-577): basic presentational steps
|
|
60
|
+
// load on every plan; advanced client steps need `actions` and runJs
|
|
61
|
+
// needs `webhooks` — see isClientStepEntitled.
|
|
62
|
+
const clientSteps = reachableSteps.filter((step)=>isClientStepEntitled(step, {
|
|
63
|
+
actionsEntitled: options.actionsEntitled,
|
|
64
|
+
allowJs: options.allowJs
|
|
65
|
+
}));
|
|
66
|
+
// Server steps only dispatch (and are only re-authorized) when the org is
|
|
67
|
+
// actions-entitled — never advertise them otherwise, so basic-tier pages
|
|
68
|
+
// don't POST to /api/events/dispatch for a no-op.
|
|
69
|
+
const hasServerSteps = options.actionsEntitled && ((_action_steps = action.steps) != null ? _action_steps : []).some((step)=>!isClientActionStep(step));
|
|
70
|
+
if (!clientSteps.length && !hasServerSteps) continue;
|
|
71
|
+
automations.push(_extends({
|
|
72
|
+
id,
|
|
73
|
+
event
|
|
74
|
+
}, ((_action_trigger2 = action.trigger) == null ? void 0 : _action_trigger2.selector) ? {
|
|
75
|
+
selector: action.trigger.selector
|
|
76
|
+
} : {}, Number((_action_trigger3 = action.trigger) == null ? void 0 : _action_trigger3.threshold) > 0 ? {
|
|
77
|
+
threshold: Number(action.trigger.threshold)
|
|
78
|
+
} : {}, ((_action_trigger4 = action.trigger) == null ? void 0 : _action_trigger4.oncePerVisitor) === true ? {
|
|
79
|
+
oncePerVisitor: true
|
|
80
|
+
} : {}, ((_action_trigger5 = action.trigger) == null ? void 0 : _action_trigger5.oncePerSession) === true ? {
|
|
81
|
+
oncePerSession: true
|
|
82
|
+
} : {}, Number((_action_trigger6 = action.trigger) == null ? void 0 : _action_trigger6.cooldownMinutes) >= 1 ? {
|
|
83
|
+
cooldownMinutes: Number((_action_trigger7 = action.trigger) == null ? void 0 : _action_trigger7.cooldownMinutes)
|
|
84
|
+
} : {}, ((_action_trigger8 = action.trigger) == null ? void 0 : _action_trigger8.everyTime) === true ? {
|
|
85
|
+
everyTime: true
|
|
86
|
+
} : {}, {
|
|
87
|
+
steps: clientSteps,
|
|
88
|
+
hasServerSteps
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
return automations;
|
|
92
|
+
}
|
|
93
|
+
export default compileClientAutomations;
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=compile-client-automations.js.map
|