@aglyn/plugins-inbox 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 +51 -0
- package/src/index.d.ts +18 -0
- package/src/index.js +19 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/contacts-card.component.d.ts +16 -0
- package/src/lib/components/contacts-card.component.js +351 -0
- package/src/lib/components/contacts-card.component.js.map +1 -0
- package/src/lib/components/inbox-attribution-zone.d.ts +52 -0
- package/src/lib/components/inbox-attribution-zone.js +53 -0
- package/src/lib/components/inbox-attribution-zone.js.map +1 -0
- package/src/lib/components/inbox-console-page.d.ts +28 -0
- package/src/lib/components/inbox-console-page.js +219 -0
- package/src/lib/components/inbox-console-page.js.map +1 -0
- package/src/lib/components/inbox-console-sections.d.ts +40 -0
- package/src/lib/components/inbox-console-sections.js +66 -0
- package/src/lib/components/inbox-console-sections.js.map +1 -0
- package/src/lib/components/inbox-glance-card.component.d.ts +26 -0
- package/src/lib/components/inbox-glance-card.component.js +215 -0
- package/src/lib/components/inbox-glance-card.component.js.map +1 -0
- package/src/lib/components/submission-list-assignment.component.d.ts +29 -0
- package/src/lib/components/submission-list-assignment.component.js +257 -0
- package/src/lib/components/submission-list-assignment.component.js.map +1 -0
- package/src/lib/components/submission-reply.component.d.ts +30 -0
- package/src/lib/components/submission-reply.component.js +249 -0
- package/src/lib/components/submission-reply.component.js.map +1 -0
- package/src/lib/components/submissions-card.component.d.ts +35 -0
- package/src/lib/components/submissions-card.component.js +577 -0
- package/src/lib/components/submissions-card.component.js.map +1 -0
- package/src/lib/components/use-record-route-context.d.ts +15 -0
- package/src/lib/components/use-record-route-context.js +41 -0
- package/src/lib/components/use-record-route-context.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/reply-policy.d.ts +134 -0
- package/src/lib/model/reply-policy.js +141 -0
- package/src/lib/model/reply-policy.js.map +1 -0
- package/src/lib/model/submission-presenter.d.ts +81 -0
- package/src/lib/model/submission-presenter.js +152 -0
- package/src/lib/model/submission-presenter.js.map +1 -0
- package/src/lib/plugin.d.ts +25 -0
- package/src/lib/plugin.js +102 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server.d.ts +179 -0
- package/src/lib/server.js +666 -0
- package/src/lib/server.js.map +1 -0
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
* How the Inbox renders one submission (AGL-2168).
|
|
19
|
+
*
|
|
20
|
+
* `/product/forms`'s hero mockup IS the Inbox, and it shows every row as a
|
|
21
|
+
* person: a coloured initials avatar, `Priya Nair`, the form name, and
|
|
22
|
+
* `2m`. The console rendered no sender at all — the row's one content cell
|
|
23
|
+
* was every field concatenated as `name: Priya Nair · email: … · message:
|
|
24
|
+
* …`, so the name was in there somewhere, at the mercy of whatever order
|
|
25
|
+
* the author happened to define the fields in.
|
|
26
|
+
*/ /**
|
|
27
|
+
* Field names that mean "this is who submitted it", most specific first.
|
|
28
|
+
*
|
|
29
|
+
* Compared against a key REDUCED to lowercase letters and digits, because
|
|
30
|
+
* the same field reaches us as `Full Name`, `full_name` and `fullname`
|
|
31
|
+
* depending on who built the form — and an exact-match lookup silently
|
|
32
|
+
* finds none of the three.
|
|
33
|
+
*/ const NAME_KEYS = [
|
|
34
|
+
'name',
|
|
35
|
+
'fullname',
|
|
36
|
+
'yourname',
|
|
37
|
+
'firstname',
|
|
38
|
+
'contactname'
|
|
39
|
+
];
|
|
40
|
+
const EMAIL_KEYS = [
|
|
41
|
+
'email',
|
|
42
|
+
'emailaddress'
|
|
43
|
+
];
|
|
44
|
+
/** Reduced key → the value the author submitted. */ function normalizeKeys(fields) {
|
|
45
|
+
const map = new Map();
|
|
46
|
+
for (const [key, value] of Object.entries(fields != null ? fields : {})){
|
|
47
|
+
const text = String(value != null ? value : '').trim();
|
|
48
|
+
if (!text) continue;
|
|
49
|
+
const reduced = key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
50
|
+
// First spelling wins, so `name` beats a later `Name`.
|
|
51
|
+
if (!map.has(reduced)) map.set(reduced, text);
|
|
52
|
+
}
|
|
53
|
+
return map;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Who sent it.
|
|
57
|
+
*
|
|
58
|
+
* A form is author-defined, so there is no guaranteed name field — the
|
|
59
|
+
* lookup is by convention and falls back to the email, then to the form's
|
|
60
|
+
* own name. It never returns an empty string: an avatar with no letters in
|
|
61
|
+
* it is worse than a generic one.
|
|
62
|
+
*/ export function submissionSender(fields, fallback = 'Someone') {
|
|
63
|
+
var _ref;
|
|
64
|
+
const map = normalizeKeys(fields);
|
|
65
|
+
const email = EMAIL_KEYS.map((key)=>map.get(key)).find(Boolean);
|
|
66
|
+
const name = NAME_KEYS.map((key)=>map.get(key)).find(Boolean);
|
|
67
|
+
const label = (_ref = name != null ? name : email) != null ? _ref : fallback;
|
|
68
|
+
return _extends({
|
|
69
|
+
label
|
|
70
|
+
}, email ? {
|
|
71
|
+
email
|
|
72
|
+
} : {}, {
|
|
73
|
+
initials: initialsOf(label)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Up to two initials. An email falls back to its local part, so
|
|
78
|
+
* `priya@lumen.co` gives `P` rather than a `@`.
|
|
79
|
+
*/ export function initialsOf(label) {
|
|
80
|
+
const trimmed = String(label != null ? label : '').trim();
|
|
81
|
+
const source = trimmed.includes('@') ? trimmed.split('@')[0] : trimmed;
|
|
82
|
+
const words = source.split(/[\s._-]+/).map((word)=>word.trim()).filter(Boolean);
|
|
83
|
+
const letters = words.slice(0, 2).map((word)=>word[0]).join('');
|
|
84
|
+
return (letters || source[0] || '?').toUpperCase();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A deterministic hue for the avatar, so one sender keeps one colour
|
|
88
|
+
* across sessions and machines. The mockup's avatars are coloured, and a
|
|
89
|
+
* random palette index would make the same person a different colour on
|
|
90
|
+
* every render.
|
|
91
|
+
*/ export function senderHue(label) {
|
|
92
|
+
let hash = 0;
|
|
93
|
+
for(let index = 0; index < label.length; index += 1){
|
|
94
|
+
hash = (hash * 31 + label.charCodeAt(index)) % 360;
|
|
95
|
+
}
|
|
96
|
+
return hash;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `2m`, `18m`, `1h`, `3d` — the relative times the mockup's list shows.
|
|
100
|
+
*
|
|
101
|
+
* The console rendered `toLocaleString()`, which is the right answer to a
|
|
102
|
+
* different question: an inbox is scanned for recency, and "8/18/2026,
|
|
103
|
+
* 2:04:31 PM" makes the reader do the subtraction. The absolute time stays
|
|
104
|
+
* on the detail pane, where it is the fact you actually want.
|
|
105
|
+
*/ export function relativeTime(atMs, nowMs = Date.now()) {
|
|
106
|
+
if (!atMs || !Number.isFinite(atMs)) return '--';
|
|
107
|
+
const seconds = Math.round((nowMs - atMs) / 1000);
|
|
108
|
+
// A clock skew between the browser and the server can put a fresh
|
|
109
|
+
// submission a few seconds in the future. "in 4 seconds" on an inbox row
|
|
110
|
+
// is a bug report waiting to happen; "now" is simply true.
|
|
111
|
+
if (seconds < 60) return 'now';
|
|
112
|
+
const minutes = Math.floor(seconds / 60);
|
|
113
|
+
if (minutes < 60) return `${minutes}m`;
|
|
114
|
+
const hours = Math.floor(minutes / 60);
|
|
115
|
+
if (hours < 24) return `${hours}h`;
|
|
116
|
+
const days = Math.floor(hours / 24);
|
|
117
|
+
if (days < 7) return `${days}d`;
|
|
118
|
+
const weeks = Math.floor(days / 7);
|
|
119
|
+
if (weeks < 5) return `${weeks}w`;
|
|
120
|
+
return `${Math.floor(days / 30)}mo`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The status chips the mockup's detail pane carries under the fields:
|
|
124
|
+
* `Saved to Inbox` (green) and `Added to "Leads" dataset` (blue).
|
|
125
|
+
*
|
|
126
|
+
* The first is unconditional and free — every submission that exists is in
|
|
127
|
+
* the Inbox, which is exactly why the mockup shows it: it is the
|
|
128
|
+
* reassurance, not a status.
|
|
129
|
+
*
|
|
130
|
+
* The second reports what the submit route actually DID. It is stamped on
|
|
131
|
+
* the document only when a record was really appended, so a form bound to
|
|
132
|
+
* a dataset that was deleted, or whose record quota was full, shows no chip
|
|
133
|
+
* rather than a chip for a row that does not exist — the two failure modes
|
|
134
|
+
* the route already swallows silently.
|
|
135
|
+
*/ export function routingChips(routing) {
|
|
136
|
+
const chips = [
|
|
137
|
+
{
|
|
138
|
+
label: 'Saved to Inbox',
|
|
139
|
+
color: 'success'
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
const dataset = routing == null ? void 0 : routing.dataset;
|
|
143
|
+
if (dataset == null ? void 0 : dataset.recordId) {
|
|
144
|
+
chips.push({
|
|
145
|
+
label: dataset.name ? `Added to “${dataset.name}” dataset` : 'Added to a dataset',
|
|
146
|
+
color: 'info'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return chips;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
//# sourceMappingURL=submission-presenter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/model/submission-presenter.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 * How the Inbox renders one submission (AGL-2168).\n *\n * `/product/forms`'s hero mockup IS the Inbox, and it shows every row as a\n * person: a coloured initials avatar, `Priya Nair`, the form name, and\n * `2m`. The console rendered no sender at all — the row's one content cell\n * was every field concatenated as `name: Priya Nair · email: … · message:\n * …`, so the name was in there somewhere, at the mercy of whatever order\n * the author happened to define the fields in.\n */\n\n/**\n * Field names that mean \"this is who submitted it\", most specific first.\n *\n * Compared against a key REDUCED to lowercase letters and digits, because\n * the same field reaches us as `Full Name`, `full_name` and `fullname`\n * depending on who built the form — and an exact-match lookup silently\n * finds none of the three.\n */\nconst NAME_KEYS = ['name', 'fullname', 'yourname', 'firstname', 'contactname']\n\nconst EMAIL_KEYS = ['email', 'emailaddress']\n\n/** Reduced key → the value the author submitted. */\nfunction normalizeKeys(\n fields: Record<string, unknown> | undefined,\n): Map<string, string> {\n const map = new Map<string, string>()\n for (const [key, value] of Object.entries(fields ?? {})) {\n const text = String(value ?? '').trim()\n if (!text) continue\n const reduced = key.toLowerCase().replace(/[^a-z0-9]/g, '')\n // First spelling wins, so `name` beats a later `Name`.\n if (!map.has(reduced)) map.set(reduced, text)\n }\n return map\n}\n\nexport interface SubmissionSender {\n /** Best available display name — a name, else an email, else a fallback. */\n label: string\n /** The email, when one was submitted. */\n email?: string\n /** One or two letters for the avatar. */\n initials: string\n}\n\n/**\n * Who sent it.\n *\n * A form is author-defined, so there is no guaranteed name field — the\n * lookup is by convention and falls back to the email, then to the form's\n * own name. It never returns an empty string: an avatar with no letters in\n * it is worse than a generic one.\n */\nexport function submissionSender(\n fields: Record<string, unknown> | undefined,\n fallback = 'Someone',\n): SubmissionSender {\n const map = normalizeKeys(fields)\n const email = EMAIL_KEYS.map((key) => map.get(key)).find(Boolean)\n const name = NAME_KEYS.map((key) => map.get(key)).find(Boolean)\n const label = name ?? email ?? fallback\n return { label, ...(email ? { email } : {}), initials: initialsOf(label) }\n}\n\n/**\n * Up to two initials. An email falls back to its local part, so\n * `priya@lumen.co` gives `P` rather than a `@`.\n */\nexport function initialsOf(label: string): string {\n const trimmed = String(label ?? '').trim()\n const source = trimmed.includes('@') ? trimmed.split('@')[0] : trimmed\n const words = source\n .split(/[\\s._-]+/)\n .map((word) => word.trim())\n .filter(Boolean)\n const letters = words\n .slice(0, 2)\n .map((word) => word[0])\n .join('')\n return (letters || source[0] || '?').toUpperCase()\n}\n\n/**\n * A deterministic hue for the avatar, so one sender keeps one colour\n * across sessions and machines. The mockup's avatars are coloured, and a\n * random palette index would make the same person a different colour on\n * every render.\n */\nexport function senderHue(label: string): number {\n let hash = 0\n for (let index = 0; index < label.length; index += 1) {\n hash = (hash * 31 + label.charCodeAt(index)) % 360\n }\n return hash\n}\n\n/**\n * `2m`, `18m`, `1h`, `3d` — the relative times the mockup's list shows.\n *\n * The console rendered `toLocaleString()`, which is the right answer to a\n * different question: an inbox is scanned for recency, and \"8/18/2026,\n * 2:04:31 PM\" makes the reader do the subtraction. The absolute time stays\n * on the detail pane, where it is the fact you actually want.\n */\nexport function relativeTime(\n atMs: number | undefined,\n nowMs: number = Date.now(),\n): string {\n if (!atMs || !Number.isFinite(atMs)) return '--'\n const seconds = Math.round((nowMs - atMs) / 1000)\n // A clock skew between the browser and the server can put a fresh\n // submission a few seconds in the future. \"in 4 seconds\" on an inbox row\n // is a bug report waiting to happen; \"now\" is simply true.\n if (seconds < 60) return 'now'\n const minutes = Math.floor(seconds / 60)\n if (minutes < 60) return `${minutes}m`\n const hours = Math.floor(minutes / 60)\n if (hours < 24) return `${hours}h`\n const days = Math.floor(hours / 24)\n if (days < 7) return `${days}d`\n const weeks = Math.floor(days / 7)\n if (weeks < 5) return `${weeks}w`\n return `${Math.floor(days / 30)}mo`\n}\n\nexport interface SubmissionRouting {\n /** The dataset a record was appended to, when one was. */\n dataset?: { id?: string; name?: string; recordId?: string }\n}\n\nexport interface RoutingChip {\n label: string\n color: 'success' | 'info' | 'default'\n}\n\n/**\n * The status chips the mockup's detail pane carries under the fields:\n * `Saved to Inbox` (green) and `Added to \"Leads\" dataset` (blue).\n *\n * The first is unconditional and free — every submission that exists is in\n * the Inbox, which is exactly why the mockup shows it: it is the\n * reassurance, not a status.\n *\n * The second reports what the submit route actually DID. It is stamped on\n * the document only when a record was really appended, so a form bound to\n * a dataset that was deleted, or whose record quota was full, shows no chip\n * rather than a chip for a row that does not exist — the two failure modes\n * the route already swallows silently.\n */\nexport function routingChips(routing: SubmissionRouting | undefined): RoutingChip[] {\n const chips: RoutingChip[] = [{ label: 'Saved to Inbox', color: 'success' }]\n const dataset = routing?.dataset\n if (dataset?.recordId) {\n chips.push({\n label: dataset.name\n ? `Added to “${dataset.name}” dataset`\n : 'Added to a dataset',\n color: 'info',\n })\n }\n return chips\n}\n"],"names":["NAME_KEYS","EMAIL_KEYS","normalizeKeys","fields","map","Map","key","value","Object","entries","text","String","trim","reduced","toLowerCase","replace","has","set","submissionSender","fallback","name","email","get","find","Boolean","label","initials","initialsOf","trimmed","source","includes","split","words","word","filter","letters","slice","join","toUpperCase","senderHue","hash","index","length","charCodeAt","relativeTime","atMs","nowMs","Date","now","Number","isFinite","seconds","Math","round","minutes","floor","hours","days","weeks","routingChips","routing","chips","color","dataset","recordId","push"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
* Inbox feature plugin (AGL-395). Console-only — form submissions, site
|
|
19
|
+
* members/leads and campaigns live in Firestore and have no canvas element,
|
|
20
|
+
* so there is no UI bundle. The console half declares the Inbox nav + its
|
|
21
|
+
* three sections through the ConsoleExtension registry (always-on). The
|
|
22
|
+
* Campaigns section and the attribution shown inside a submission are zones
|
|
23
|
+
* this plugin hosts; the plugin that owns campaigns draws in them.
|
|
24
|
+
*/
|
|
25
|
+
export declare function registerInboxConsole(): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
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 * as Aglyn from "@aglyn/aglyn";
|
|
17
|
+
import { mdiInboxArrowDown } from "@aglyn/shared-data-mdi";
|
|
18
|
+
import { lazy } from "react";
|
|
19
|
+
import { INBOX_CONSOLE_SECTIONS } from "./components/inbox-console-sections.js";
|
|
20
|
+
import { registerPluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
|
|
21
|
+
import { INBOX_CAMPAIGNS_ZONE, INBOX_RECORD_ATTRIBUTION_ZONE } from "./components/inbox-attribution-zone.js";
|
|
22
|
+
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
23
|
+
/** Code-split: the Inbox console page only loads when opened. */ const InboxConsolePage = lazy(()=>import("./components/inbox-console-page.js"));
|
|
24
|
+
/**
|
|
25
|
+
* Code-split: the submissions table loads when a form page asks for it. The
|
|
26
|
+
* zone hands `hostId` and `formId`, which is what the table's own props are.
|
|
27
|
+
*/ const FormSubmissionsReader = lazy(()=>import("./components/submissions-card.component.js"));
|
|
28
|
+
/** Dashboard glance card, loaded only where the shell renders the slot. */ const InboxGlanceCard = lazy(()=>import("./components/inbox-glance-card.component.js"));
|
|
29
|
+
/**
|
|
30
|
+
* Inbox feature plugin (AGL-395). Console-only — form submissions, site
|
|
31
|
+
* members/leads and campaigns live in Firestore and have no canvas element,
|
|
32
|
+
* so there is no UI bundle. The console half declares the Inbox nav + its
|
|
33
|
+
* three sections through the ConsoleExtension registry (always-on). The
|
|
34
|
+
* Campaigns section and the attribution shown inside a submission are zones
|
|
35
|
+
* this plugin hosts; the plugin that owns campaigns draws in them.
|
|
36
|
+
*/ export function registerInboxConsole() {
|
|
37
|
+
registerPluginZone({
|
|
38
|
+
zone: INBOX_RECORD_ATTRIBUTION_ZONE,
|
|
39
|
+
label: 'Where a lead or a submission came from',
|
|
40
|
+
surface: 'console',
|
|
41
|
+
description: 'In a lead’s “Where this came from” dialog and under an open submission. A widget here says which campaign or link brought it; it is handed the site, what the record is and its id, and it writes nothing.'
|
|
42
|
+
}, // Named, because a spec calls this registrar without the loader.
|
|
43
|
+
{
|
|
44
|
+
pluginId: BUNDLE_ID
|
|
45
|
+
});
|
|
46
|
+
registerPluginZone({
|
|
47
|
+
zone: INBOX_CAMPAIGNS_ZONE,
|
|
48
|
+
label: 'The Inbox’s Campaigns section',
|
|
49
|
+
surface: 'console',
|
|
50
|
+
description: 'The body of the Inbox’s Campaigns tab. A widget here lists the site’s campaigns; it is handed the site and nothing else.'
|
|
51
|
+
}, {
|
|
52
|
+
pluginId: BUNDLE_ID
|
|
53
|
+
});
|
|
54
|
+
Aglyn.registerConsoleExtension({
|
|
55
|
+
pluginId: BUNDLE_ID,
|
|
56
|
+
displayName: 'Inbox',
|
|
57
|
+
// The host dashboard's inbox glance. A form submission is the one thing
|
|
58
|
+
// on a site that is waiting for a REPLY, and until this it was two
|
|
59
|
+
// clicks from the page an owner opens first.
|
|
60
|
+
widgets: [
|
|
61
|
+
{
|
|
62
|
+
slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,
|
|
63
|
+
widgetId: 'inbox-glance',
|
|
64
|
+
title: 'Inbox',
|
|
65
|
+
Component: InboxGlanceCard
|
|
66
|
+
},
|
|
67
|
+
// One form's submissions, on the form's own page. The forms plugin
|
|
68
|
+
// hosts the zone and hands it the site and the form; the reader is this
|
|
69
|
+
// plugin's table, narrowed to that form, so the form page gets the same
|
|
70
|
+
// ordered, paged walk the Inbox uses without importing it.
|
|
71
|
+
{
|
|
72
|
+
slot: 'formSubmissions',
|
|
73
|
+
widgetId: 'inbox-form-submissions',
|
|
74
|
+
title: 'Submissions',
|
|
75
|
+
Component: FormSubmissionsReader
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
navItems: [
|
|
79
|
+
{
|
|
80
|
+
label: 'Inbox',
|
|
81
|
+
href: '/inbox',
|
|
82
|
+
// Sections as ROUTES (AGL-2501): each is a real URL the shell
|
|
83
|
+
// resolves and gates, so the page mounts the one being read.
|
|
84
|
+
sections: INBOX_CONSOLE_SECTIONS,
|
|
85
|
+
navTabId: 'nav-tab-inbox',
|
|
86
|
+
icon: {
|
|
87
|
+
path: mdiInboxArrowDown.path
|
|
88
|
+
},
|
|
89
|
+
header: {
|
|
90
|
+
title: 'Inbox',
|
|
91
|
+
icon: {
|
|
92
|
+
path: mdiInboxArrowDown.path
|
|
93
|
+
},
|
|
94
|
+
docsTopic: 'forms'
|
|
95
|
+
},
|
|
96
|
+
Component: InboxConsolePage
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/inbox/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiInboxArrowDown } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { INBOX_CONSOLE_SECTIONS } from './components/inbox-console-sections'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport {\n INBOX_CAMPAIGNS_ZONE,\n INBOX_RECORD_ATTRIBUTION_ZONE,\n} from './components/inbox-attribution-zone'\nimport { BUNDLE_ID } from './constants/bundle-common'\n\n/** Code-split: the Inbox console page only loads when opened. */\nconst InboxConsolePage = lazy(() => import('./components/inbox-console-page'))\n\n/**\n * Code-split: the submissions table loads when a form page asks for it. The\n * zone hands `hostId` and `formId`, which is what the table's own props are.\n */\nconst FormSubmissionsReader = lazy(\n () => import('./components/submissions-card.component'),\n)\n\n/** Dashboard glance card, loaded only where the shell renders the slot. */\nconst InboxGlanceCard = lazy(\n () => import('./components/inbox-glance-card.component'),\n)\n\n/**\n * Inbox feature plugin (AGL-395). Console-only — form submissions, site\n * members/leads and campaigns live in Firestore and have no canvas element,\n * so there is no UI bundle. The console half declares the Inbox nav + its\n * three sections through the ConsoleExtension registry (always-on). The\n * Campaigns section and the attribution shown inside a submission are zones\n * this plugin hosts; the plugin that owns campaigns draws in them.\n */\nexport function registerInboxConsole(): void {\n registerPluginZone(\n {\n zone: INBOX_RECORD_ATTRIBUTION_ZONE,\n label: 'Where a lead or a submission came from',\n surface: 'console',\n description:\n 'In a lead’s “Where this came from” dialog and under an open submission. A widget here says which campaign or link brought it; it is handed the site, what the record is and its id, and it writes nothing.',\n },\n // Named, because a spec calls this registrar without the loader.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: INBOX_CAMPAIGNS_ZONE,\n label: 'The Inbox’s Campaigns section',\n surface: 'console',\n description:\n 'The body of the Inbox’s Campaigns tab. A widget here lists the site’s campaigns; it is handed the site and nothing else.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Inbox',\n // The host dashboard's inbox glance. A form submission is the one thing\n // on a site that is waiting for a REPLY, and until this it was two\n // clicks from the page an owner opens first.\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'inbox-glance',\n title: 'Inbox',\n Component: InboxGlanceCard,\n },\n // One form's submissions, on the form's own page. The forms plugin\n // hosts the zone and hands it the site and the form; the reader is this\n // plugin's table, narrowed to that form, so the form page gets the same\n // ordered, paged walk the Inbox uses without importing it.\n {\n slot: 'formSubmissions',\n widgetId: 'inbox-form-submissions',\n title: 'Submissions',\n Component: FormSubmissionsReader,\n },\n ],\n navItems: [\n {\n label: 'Inbox',\n href: '/inbox',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: INBOX_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-inbox',\n icon: { path: mdiInboxArrowDown.path },\n header: {\n title: 'Inbox',\n icon: { path: mdiInboxArrowDown.path },\n docsTopic: 'forms',\n },\n Component: InboxConsolePage,\n },\n ],\n })\n}\n"],"names":["Aglyn","mdiInboxArrowDown","lazy","INBOX_CONSOLE_SECTIONS","registerPluginZone","INBOX_CAMPAIGNS_ZONE","INBOX_RECORD_ATTRIBUTION_ZONE","BUNDLE_ID","InboxConsolePage","FormSubmissionsReader","InboxGlanceCard","registerInboxConsole","zone","label","surface","description","pluginId","registerConsoleExtension","displayName","widgets","slot","CONSOLE_WIDGET_SLOTS","hostDashboard","widgetId","title","Component","navItems","href","sections","navTabId","icon","path","header","docsTopic"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,iBAAiB,QAAQ,yBAAwB;AAC1D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,sBAAsB,QAAQ,yCAAqC;AAC5E,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SACEC,oBAAoB,EACpBC,6BAA6B,QACxB,yCAAqC;AAC5C,SAASC,SAAS,QAAQ,+BAA2B;AAErD,+DAA+D,GAC/D,MAAMC,mBAAmBN,KAAK,IAAM,MAAM,CAAC;AAE3C;;;CAGC,GACD,MAAMO,wBAAwBP,KAC5B,IAAM,MAAM,CAAC;AAGf,yEAAyE,GACzE,MAAMQ,kBAAkBR,KACtB,IAAM,MAAM,CAAC;AAGf;;;;;;;CAOC,GACD,OAAO,SAASS;IACdP,mBACE;QACEQ,MAAMN;QACNO,OAAO;QACPC,SAAS;QACTC,aACE;IACJ,GACA,iEAAiE;IACjE;QAAEC,UAAUT;IAAU;IAExBH,mBACE;QACEQ,MAAMP;QACNQ,OAAO;QACPC,SAAS;QACTC,aACE;IACJ,GACA;QAAEC,UAAUT;IAAU;IAExBP,MAAMiB,wBAAwB,CAAC;QAC7BD,UAAUT;QACVW,aAAa;QACb,wEAAwE;QACxE,mEAAmE;QACnE,6CAA6C;QAC7CC,SAAS;YACP;gBACEC,MAAMpB,MAAMqB,oBAAoB,CAACC,aAAa;gBAC9CC,UAAU;gBACVC,OAAO;gBACPC,WAAWf;YACb;YACA,mEAAmE;YACnE,wEAAwE;YACxE,wEAAwE;YACxE,2DAA2D;YAC3D;gBACEU,MAAM;gBACNG,UAAU;gBACVC,OAAO;gBACPC,WAAWhB;YACb;SACD;QACDiB,UAAU;YACR;gBACEb,OAAO;gBACPc,MAAM;gBACN,8DAA8D;gBAC9D,6DAA6D;gBAC7DC,UAAUzB;gBACV0B,UAAU;gBACVC,MAAM;oBAAEC,MAAM9B,kBAAkB8B,IAAI;gBAAC;gBACrCC,QAAQ;oBACNR,OAAO;oBACPM,MAAM;wBAAEC,MAAM9B,kBAAkB8B,IAAI;oBAAC;oBACrCE,WAAW;gBACb;gBACAR,WAAWjB;YACb;SACD;IACH;AACF"}
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
* The Inbox's two acts on the person who sent a submission: answer them, and
|
|
19
|
+
* put them on a marketing list.
|
|
20
|
+
*
|
|
21
|
+
* `POST inbox/reply` sends one message. `POST inbox/assign-list` enrolls, and
|
|
22
|
+
* `POST inbox/list-options` tells the merchant what they may do before they
|
|
23
|
+
* do it.
|
|
24
|
+
*
|
|
25
|
+
* ## The two acts are kept apart everywhere
|
|
26
|
+
*
|
|
27
|
+
* A reply is TRANSACTIONAL — the person asked to be contacted by submitting
|
|
28
|
+
* the form, and answering them is the transaction they started. A list is
|
|
29
|
+
* MARKETING — a standing invitation to mail them about things they never
|
|
30
|
+
* asked about. So they are separate routes, separate records and separate
|
|
31
|
+
* cards on screen, and neither is a side effect of the other: replying
|
|
32
|
+
* enrolls nobody and writes no consent, and enrolling sends nothing and
|
|
33
|
+
* meters nothing. Folding either into the other would make a merchant's
|
|
34
|
+
* ordinary act of answering a customer into an act with consequences they
|
|
35
|
+
* did not choose. The enrollment rule itself is the framework's
|
|
36
|
+
* `list-assignment-policy`, shared with the Emails console's audience card so
|
|
37
|
+
* that both surfaces refuse the same people for the same stated reasons.
|
|
38
|
+
*
|
|
39
|
+
* ## The boundary this feature sits on, stated because the UI must say it too
|
|
40
|
+
*
|
|
41
|
+
* The Inbox is not a mailbox. Nothing in the INBOX receives mail: no route
|
|
42
|
+
* of its own, no MX record pointed at a submission, no parser. A submission
|
|
43
|
+
* arrived as an HTTP POST, not as a message, so there is no `Message-ID` to
|
|
44
|
+
* thread against and a reply is always the FIRST message in its conversation.
|
|
45
|
+
* (The platform's one receiving door is the CRM's capture address —
|
|
46
|
+
* `POST /api/crm/inbound` in the console app, AGL-2657 — which files a
|
|
47
|
+
* forwarded or copied message on a contact's timeline. It threads nothing
|
|
48
|
+
* here: a submission is not mail, and the Inbox's statement stands.)
|
|
49
|
+
*
|
|
50
|
+
* That decides two things:
|
|
51
|
+
*
|
|
52
|
+
* - **No `In-Reply-To` or `References` header is sent.** There is nothing to
|
|
53
|
+
* put in them. Inventing an identifier would produce headers that reference
|
|
54
|
+
* a message no mail server has ever seen, which threads nothing and makes
|
|
55
|
+
* some filters treat the message as forged.
|
|
56
|
+
* - **`Reply-To` is the sender's own console account address**, so when the
|
|
57
|
+
* recipient answers, the answer reaches a human in a real mailbox. It does
|
|
58
|
+
* not come back here, and the composer says so. A reply that went to the
|
|
59
|
+
* platform's unmonitored address would be a message the merchant never sees
|
|
60
|
+
* and the customer believes was received.
|
|
61
|
+
*
|
|
62
|
+
* The thread the merchant sees is ours, stored under the submission, and it
|
|
63
|
+
* holds what we sent — never what came back, because nothing comes back.
|
|
64
|
+
*
|
|
65
|
+
* ## Sending identity
|
|
66
|
+
*
|
|
67
|
+
* The `From:` address is whatever THIS SITE sends as — `hostSendingIdentity`,
|
|
68
|
+
* the same resolution a campaign gets — so a site with a domain of its own
|
|
69
|
+
* replies on it, and a site without one replies on the shared pool, which is
|
|
70
|
+
* correct for a reply because a reply is transactional.
|
|
71
|
+
*
|
|
72
|
+
* The display name varies separately, through `resolveBrandingProfile`, which
|
|
73
|
+
* is entitlement-gated: a merchant without white-label replies under the
|
|
74
|
+
* platform's name even on their own domain.
|
|
75
|
+
*
|
|
76
|
+
* `Reply-To` stays load-bearing either way, and not as a stopgap. Nothing
|
|
77
|
+
* here receives mail, so the answer has to be routed to a mailbox somebody
|
|
78
|
+
* actually reads — and `Reply-To` is the one header that may name an address
|
|
79
|
+
* on a domain this platform has never verified.
|
|
80
|
+
*
|
|
81
|
+
* ## Consent and suppression
|
|
82
|
+
*
|
|
83
|
+
* A reply is transactional: the recipient asked to be contacted by submitting
|
|
84
|
+
* the form, so no marketing-consent record is required and none is read. The
|
|
85
|
+
* suppression lists still apply — both of them — and this is the first send
|
|
86
|
+
* path in the product to consult BOTH on one address:
|
|
87
|
+
*
|
|
88
|
+
* - the platform list, through `isEmailSuppressed`, which fails closed;
|
|
89
|
+
* - this site's list, under `hosts/{hostId}/suppressions`.
|
|
90
|
+
*
|
|
91
|
+
* Both are keyed with `emailSuppressionKey`, one derivation, so the two reads
|
|
92
|
+
* cannot disagree about which document to look for.
|
|
93
|
+
*/
|
|
94
|
+
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
95
|
+
import { type ReplyRefusal } from './model/reply-policy';
|
|
96
|
+
/** Where a reply is stored, under the submission it answers. */
|
|
97
|
+
export declare const REPLIES_SUBCOLLECTION = "replies";
|
|
98
|
+
/** `context` on the send: the log label and the Resend attribution tag. */
|
|
99
|
+
export declare const REPLY_CONTEXT = "inbox-reply";
|
|
100
|
+
/**
|
|
101
|
+
* Is this address suppressed, on either list?
|
|
102
|
+
*
|
|
103
|
+
* Order matters only for the reason reported, not the outcome. The platform
|
|
104
|
+
* list is read first because it is the one that carries a bounce learned
|
|
105
|
+
* anywhere — including on a send that named no site — and that is the more
|
|
106
|
+
* useful thing to tell a merchant who is about to retype the message.
|
|
107
|
+
*
|
|
108
|
+
* Named for the ADDRESS rather than for either act, because both use it: a
|
|
109
|
+
* reply must not go to a dead or complaining mailbox, and a list enrollment
|
|
110
|
+
* must not put one on a standing audience. A second copy for the second act
|
|
111
|
+
* would be two answers to "may this address be mailed".
|
|
112
|
+
*/
|
|
113
|
+
export declare function addressSuppression(hostId: string, email: string, firestore?: unknown): Promise<ReplyRefusal | null>;
|
|
114
|
+
/**
|
|
115
|
+
* Reply to one submission.
|
|
116
|
+
*
|
|
117
|
+
* Body: `{ hostId, submissionId, subject, message }`. The recipient is
|
|
118
|
+
* deliberately absent — it is read off the stored submission, so this route
|
|
119
|
+
* cannot be pointed at an address of the caller's choosing.
|
|
120
|
+
*/
|
|
121
|
+
export declare const inboxReplyHandler: PluginApiHandler;
|
|
122
|
+
/** Where an assignment is recorded, under the submission that occasioned it. */
|
|
123
|
+
export declare const LIST_ASSIGNMENTS_SUBCOLLECTION = "listAssignments";
|
|
124
|
+
/** The `source` stamped on a member enrolled from the Inbox. */
|
|
125
|
+
export declare const ASSIGNMENT_SOURCE = "inbox";
|
|
126
|
+
/**
|
|
127
|
+
* How many lists the picker offers.
|
|
128
|
+
*
|
|
129
|
+
* A ceiling on the READ, so a merchant with an unusual number of lists costs
|
|
130
|
+
* one bounded query rather than a scan, and the response says when it is a
|
|
131
|
+
* floor rather than presenting a slice as the whole set.
|
|
132
|
+
*/
|
|
133
|
+
export declare const LIST_OPTIONS_LIMIT = 100;
|
|
134
|
+
/**
|
|
135
|
+
* `POST inbox/list-options` — what the merchant may do with this sender.
|
|
136
|
+
*
|
|
137
|
+
* Reads only. It exists because the answer needs three things the browser
|
|
138
|
+
* cannot have: the org's lists (rules put them behind org-wide membership,
|
|
139
|
+
* which the acting console session may not hold), the person's consent record
|
|
140
|
+
* (an org contact, same gate), and both suppression lists. Computing any of
|
|
141
|
+
* it client-side would be a second copy of the rule, on the surface whose
|
|
142
|
+
* whole job is to tell the merchant the truth about what is about to happen.
|
|
143
|
+
*
|
|
144
|
+
* Reached by an explicit expansion in the reader, never on mount: it is three
|
|
145
|
+
* reads and a bounded query, and paying them once per opened submission would
|
|
146
|
+
* charge every merchant who never touches lists.
|
|
147
|
+
*/
|
|
148
|
+
export declare const inboxListOptionsHandler: PluginApiHandler;
|
|
149
|
+
/**
|
|
150
|
+
* `POST inbox/assign-list` — put this sender on a marketing list.
|
|
151
|
+
*
|
|
152
|
+
* Body: `{ hostId, submissionId, listId, attestConsent? }`. `attestConsent` is
|
|
153
|
+
* the merchant STATING that they have this person's permission; it is not a
|
|
154
|
+
* way to name a basis, because the pass-through basis is derived server-side
|
|
155
|
+
* from the person's own record.
|
|
156
|
+
*
|
|
157
|
+
* ## What this act is, and what it is not
|
|
158
|
+
*
|
|
159
|
+
* It is marketing enrollment: the only consumer of a list is a campaign. It
|
|
160
|
+
* is therefore kept entirely separate from the reply — different route,
|
|
161
|
+
* different card in the UI, different record — and replying enrolls nobody.
|
|
162
|
+
* The reverse holds too: enrolling somebody sends them nothing, meters
|
|
163
|
+
* nothing, and is not a promise that they are still mailable when a campaign
|
|
164
|
+
* eventually runs. Suppression is consulted again at send time, in
|
|
165
|
+
* `filterSendableForHost`, because an address can be suppressed the day after
|
|
166
|
+
* it is enrolled and an enrollment-time check that licensed every later send
|
|
167
|
+
* would be a check that passes once and pays out forever.
|
|
168
|
+
*/
|
|
169
|
+
export declare const inboxAssignListHandler: PluginApiHandler;
|
|
170
|
+
/**
|
|
171
|
+
* Console API registration.
|
|
172
|
+
*
|
|
173
|
+
* None of these is on the machine-path exemption list in
|
|
174
|
+
* `plugin-api-rate-limit.ts`. Each is reached by a person pressing a button in
|
|
175
|
+
* a browser, so the visitor limiter's per-(site, IP) budget is far above any
|
|
176
|
+
* real use of them and is the right ceiling for surfaces that put mail on the
|
|
177
|
+
* wire or a person into a marketing audience.
|
|
178
|
+
*/
|
|
179
|
+
export declare function registerInboxConsoleApi(): void;
|