@aglyn/plugins-inbox 1.0.0-beta.179 → 1.0.0-beta.181
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -10
- package/src/lib/components/contacts-card.component.d.ts +10 -2
- package/src/lib/components/contacts-card.component.js +94 -41
- package/src/lib/components/contacts-card.component.js.map +1 -1
- package/src/lib/components/inbox-attribution-zone.d.ts +9 -1
- package/src/lib/components/inbox-attribution-zone.js +1 -1
- package/src/lib/components/inbox-attribution-zone.js.map +1 -1
- package/src/lib/components/inbox-console-page.d.ts +4 -0
- package/src/lib/components/inbox-console-page.js +119 -13
- package/src/lib/components/inbox-console-page.js.map +1 -1
- package/src/lib/components/inbox-console-sections.d.ts +13 -0
- package/src/lib/components/inbox-console-sections.js +18 -3
- package/src/lib/components/inbox-console-sections.js.map +1 -1
- package/src/lib/components/inbox-glance-card.component.js +6 -1
- package/src/lib/components/inbox-glance-card.component.js.map +1 -1
- package/src/lib/components/inbox-org-sites.d.ts +27 -0
- package/src/lib/components/inbox-org-sites.js +31 -0
- package/src/lib/components/inbox-org-sites.js.map +1 -0
- package/src/lib/components/submissions-card.component.d.ts +10 -2
- package/src/lib/components/submissions-card.component.js +89 -29
- package/src/lib/components/submissions-card.component.js.map +1 -1
- package/src/lib/components/use-inbox-site-pick.d.ts +38 -0
- package/src/lib/components/use-inbox-site-pick.js +99 -0
- package/src/lib/components/use-inbox-site-pick.js.map +1 -0
- package/src/lib/components/use-record-route-context.d.ts +8 -3
- package/src/lib/components/use-record-route-context.js +10 -6
- package/src/lib/components/use-record-route-context.js.map +1 -1
- package/src/lib/plugin.d.ts +4 -3
- package/src/lib/plugin.js +35 -5
- package/src/lib/plugin.js.map +1 -1
|
@@ -18,11 +18,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
import { formSpamCaughtNotice, formSubmissionsPausedNotice, submissionMonthKey, visitorRecordRefusedCounterId, visitorRecordsPausedNotice } from "@aglyn/aglyn";
|
|
19
19
|
import { HubSections } from "@aglyn/shared-ui-next";
|
|
20
20
|
import { useFirestore, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
|
|
21
|
-
import { Alert, AlertTitle, Typography } from "@mui/material";
|
|
21
|
+
import { Alert, AlertTitle, Box, CircularProgress, MenuItem, TextField, Typography } from "@mui/material";
|
|
22
22
|
import { doc } from "firebase/firestore";
|
|
23
23
|
import ContactsCard from "./contacts-card.component.js";
|
|
24
24
|
import { InboxCampaignsZone } from "./inbox-attribution-zone.js";
|
|
25
25
|
import SubmissionsCard from "./submissions-card.component.js";
|
|
26
|
+
import { useInboxSitePick } from "./use-inbox-site-pick.js";
|
|
26
27
|
/**
|
|
27
28
|
* The body of one inbox section, built only when that section is the one being
|
|
28
29
|
* read (AGL-2501).
|
|
@@ -49,6 +50,38 @@ import SubmissionsCard from "./submissions-card.component.js";
|
|
|
49
50
|
return null;
|
|
50
51
|
}
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The body of one section on the ORGANIZATION's Inbox (AGL-3303): every
|
|
55
|
+
* site's, with no site picked — the submissions in one collection-group
|
|
56
|
+
* list, the organization's leads, every campaign — and exactly the site
|
|
57
|
+
* view, through the same cards, once one is.
|
|
58
|
+
*
|
|
59
|
+
* Handing a picked site to the site body is the point rather than a
|
|
60
|
+
* shortcut: that site's form filter, its members and its campaigns are
|
|
61
|
+
* facts only a site has, and the cards that already read them are the ones
|
|
62
|
+
* that read them correctly.
|
|
63
|
+
*/ function orgSectionBody(section, mount, siteHostId) {
|
|
64
|
+
if (siteHostId) return sectionBody(section, siteHostId);
|
|
65
|
+
switch(section){
|
|
66
|
+
case 'submissions':
|
|
67
|
+
return /*#__PURE__*/ _jsx(SubmissionsCard, {
|
|
68
|
+
hostId: null,
|
|
69
|
+
orgMount: mount
|
|
70
|
+
});
|
|
71
|
+
case 'contacts':
|
|
72
|
+
return /*#__PURE__*/ _jsx(ContactsCard, {
|
|
73
|
+
hostId: null,
|
|
74
|
+
orgMount: mount
|
|
75
|
+
});
|
|
76
|
+
case 'campaigns':
|
|
77
|
+
return /*#__PURE__*/ _jsx(InboxCampaignsZone, {
|
|
78
|
+
hostId: null,
|
|
79
|
+
orgMount: mount
|
|
80
|
+
});
|
|
81
|
+
default:
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
52
85
|
/**
|
|
53
86
|
* Inbox (AGL-77/104/109 → AGL-395): form submissions reader, site members +
|
|
54
87
|
* leads, and campaigns — owned by the inbox plugin and rendered by the shell's
|
|
@@ -70,10 +103,25 @@ import SubmissionsCard from "./submissions-card.component.js";
|
|
|
70
103
|
* others do not exist to subscribe. What routing adds besides is that the URL
|
|
71
104
|
* names the section: it is linkable, the back button walks sections, and the
|
|
72
105
|
* breadcrumb says where you are.
|
|
106
|
+
*
|
|
107
|
+
* The ORGANIZATION's Inbox is this page handed no site (AGL-3303): the same
|
|
108
|
+
* sections over every site at once, and a site filter above the rail that
|
|
109
|
+
* narrows the whole page to one site's own view.
|
|
73
110
|
*/ export function InboxConsolePage(props) {
|
|
74
111
|
var _ref, _ref1, _ref2, _ref3;
|
|
75
|
-
const { hostId, section, sections, basePath } = props;
|
|
112
|
+
const { hostId, orgMount, section, sections, basePath } = props;
|
|
76
113
|
const firestore = useFirestore();
|
|
114
|
+
/*
|
|
115
|
+
* On the organization's Inbox, the site the page is narrowed to — `null`
|
|
116
|
+
* for every site (AGL-3303). Under a site there is no mount and no pick.
|
|
117
|
+
*/ const sitePick = useInboxSitePick(hostId == null ? orgMount : undefined);
|
|
118
|
+
/*
|
|
119
|
+
* The site whose ceilings the notices below read: this page's own, or the
|
|
120
|
+
* one picked on the organization's Inbox. With every site listed there is
|
|
121
|
+
* no one site to read them for, and reading every site's four counters
|
|
122
|
+
* would be a listener per site on a page that promises a bounded read, so
|
|
123
|
+
* the notices wait for a pick.
|
|
124
|
+
*/ const noticeHostId = hostId != null ? hostId : sitePick.hostId;
|
|
77
125
|
// Submissions this site's abuse ceiling refused (AGL-1655 → AGL-1666).
|
|
78
126
|
//
|
|
79
127
|
// Until this, the refusal existed in two places a site owner cannot see: a
|
|
@@ -83,9 +131,9 @@ import SubmissionsCard from "./submissions-card.component.js";
|
|
|
83
131
|
// is never created and cannot be recovered by unmuting. This surface is
|
|
84
132
|
// the durable one, and it is a plain read of the same document the
|
|
85
133
|
// dropped-contacts alert uses (AGL-891).
|
|
86
|
-
const { data: refusedCounter } = useFirestoreDoc(()=>doc(firestore, 'hosts',
|
|
134
|
+
const { data: refusedCounter } = useFirestoreDoc(()=>noticeHostId ? doc(firestore, 'hosts', noticeHostId, 'counters', 'formSubmissionsRefused') : null, [
|
|
87
135
|
firestore,
|
|
88
|
-
|
|
136
|
+
noticeHostId
|
|
89
137
|
]);
|
|
90
138
|
// Keyed by the month the SERVER wrote, via the shared helper — a key
|
|
91
139
|
// derived differently here would read zero refusals on exactly the sites
|
|
@@ -102,9 +150,9 @@ import SubmissionsCard from "./submissions-card.component.js";
|
|
|
102
150
|
// same host-admin read the rules already grant (AGL-1367), same shared
|
|
103
151
|
// month key — and the shared sentence returns null below one catch, so a
|
|
104
152
|
// quiet month renders nothing rather than a reassuring zero.
|
|
105
|
-
const { data: spamCounter } = useFirestoreDoc(()=>doc(firestore, 'hosts',
|
|
153
|
+
const { data: spamCounter } = useFirestoreDoc(()=>noticeHostId ? doc(firestore, 'hosts', noticeHostId, 'counters', 'formSubmissionsSpam') : null, [
|
|
106
154
|
firestore,
|
|
107
|
-
|
|
155
|
+
noticeHostId
|
|
108
156
|
]);
|
|
109
157
|
const spamNotice = formSpamCaughtNotice({
|
|
110
158
|
spam: Number((_ref1 = spamCounter == null ? void 0 : spamCounter[submissionMonthKey()]) != null ? _ref1 : 0)
|
|
@@ -124,18 +172,18 @@ import SubmissionsCard from "./submissions-card.component.js";
|
|
|
124
172
|
// implemented. Without it a refusal exists in two places a site owner
|
|
125
173
|
// cannot see: a Firestore document only our console renders, and one
|
|
126
174
|
// notification that `system.` bucket-muting can suppress at write time.
|
|
127
|
-
const { data: membersRefusedCounter } = useFirestoreDoc(()=>doc(firestore, 'hosts',
|
|
175
|
+
const { data: membersRefusedCounter } = useFirestoreDoc(()=>noticeHostId ? doc(firestore, 'hosts', noticeHostId, 'counters', visitorRecordRefusedCounterId('siteMembers')) : null, [
|
|
128
176
|
firestore,
|
|
129
|
-
|
|
177
|
+
noticeHostId
|
|
130
178
|
]);
|
|
131
179
|
const membersPausedNotice = visitorRecordsPausedNotice({
|
|
132
180
|
kind: 'siteMembers',
|
|
133
181
|
refused: Number((_ref2 = membersRefusedCounter == null ? void 0 : membersRefusedCounter[submissionMonthKey()]) != null ? _ref2 : 0),
|
|
134
182
|
ceiling: Number(membersRefusedCounter == null ? void 0 : membersRefusedCounter['ceiling']) || undefined
|
|
135
183
|
});
|
|
136
|
-
const { data: leadsRefusedCounter } = useFirestoreDoc(()=>doc(firestore, 'hosts',
|
|
184
|
+
const { data: leadsRefusedCounter } = useFirestoreDoc(()=>noticeHostId ? doc(firestore, 'hosts', noticeHostId, 'counters', visitorRecordRefusedCounterId('leads')) : null, [
|
|
137
185
|
firestore,
|
|
138
|
-
|
|
186
|
+
noticeHostId
|
|
139
187
|
]);
|
|
140
188
|
const leadsPausedNotice = visitorRecordsPausedNotice({
|
|
141
189
|
kind: 'leads',
|
|
@@ -151,9 +199,9 @@ import SubmissionsCard from "./submissions-card.component.js";
|
|
|
151
199
|
* The four counter reads above run either way, and that is deliberate: they
|
|
152
200
|
* are four single-document listens, and hoisting them behind this guard
|
|
153
201
|
* would make a ceiling notice appear a frame late on the section a reader
|
|
154
|
-
* lands on.
|
|
202
|
+
* lands on. (On the organization's Inbox they run once a site is picked.)
|
|
155
203
|
*/ if (!section || !(sections == null ? void 0 : sections.length) || !basePath) return null;
|
|
156
|
-
|
|
204
|
+
const notices = /*#__PURE__*/ _jsxs(_Fragment, {
|
|
157
205
|
children: [
|
|
158
206
|
pausedNotice ? /*#__PURE__*/ _jsxs(Alert, {
|
|
159
207
|
severity: "warning",
|
|
@@ -205,7 +253,65 @@ import SubmissionsCard from "./submissions-card.component.js";
|
|
|
205
253
|
children: notice.message
|
|
206
254
|
})
|
|
207
255
|
]
|
|
208
|
-
}, notice.title) : null)
|
|
256
|
+
}, notice.title) : null)
|
|
257
|
+
]
|
|
258
|
+
});
|
|
259
|
+
if (hostId == null) {
|
|
260
|
+
var _sitePick_hostId;
|
|
261
|
+
// No site and no org to stand in for it: nothing this page can scope.
|
|
262
|
+
if (!orgMount) return null;
|
|
263
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
264
|
+
children: [
|
|
265
|
+
sitePick.options.length ? /*#__PURE__*/ _jsxs(TextField, {
|
|
266
|
+
select: true,
|
|
267
|
+
size: "small",
|
|
268
|
+
label: 'Site',
|
|
269
|
+
value: (_sitePick_hostId = sitePick.hostId) != null ? _sitePick_hostId : '',
|
|
270
|
+
onChange: (event)=>sitePick.setHostId(event.target.value || null),
|
|
271
|
+
// "All sites" is the empty value, which a select would otherwise
|
|
272
|
+
// draw as a blank rather than as the choice it is — and the label
|
|
273
|
+
// is shrunk with it, or it paints over that choice.
|
|
274
|
+
slotProps: {
|
|
275
|
+
select: {
|
|
276
|
+
displayEmpty: true
|
|
277
|
+
},
|
|
278
|
+
inputLabel: {
|
|
279
|
+
shrink: true
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
sx: {
|
|
283
|
+
mb: 2,
|
|
284
|
+
minWidth: 240
|
|
285
|
+
},
|
|
286
|
+
children: [
|
|
287
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
288
|
+
value: "",
|
|
289
|
+
children: 'All sites'
|
|
290
|
+
}),
|
|
291
|
+
sitePick.options.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
292
|
+
value: option.value,
|
|
293
|
+
children: option.label
|
|
294
|
+
}, option.value))
|
|
295
|
+
]
|
|
296
|
+
}) : null,
|
|
297
|
+
notices,
|
|
298
|
+
/*#__PURE__*/ _jsx(HubSections, {
|
|
299
|
+
sections: sections,
|
|
300
|
+
children: sitePick.ready ? orgSectionBody(section, orgMount, sitePick.hostId) : /*#__PURE__*/ _jsx(Box, {
|
|
301
|
+
sx: {
|
|
302
|
+
p: 2
|
|
303
|
+
},
|
|
304
|
+
children: /*#__PURE__*/ _jsx(CircularProgress, {
|
|
305
|
+
size: 24
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
]
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
313
|
+
children: [
|
|
314
|
+
notices,
|
|
209
315
|
/*#__PURE__*/ _jsx(HubSections, {
|
|
210
316
|
sections: sections,
|
|
211
317
|
children: sectionBody(section, hostId)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsolePluginPageProps,\n formSpamCaughtNotice,\n formSubmissionsPausedNotice,\n submissionMonthKey,\n visitorRecordRefusedCounterId,\n visitorRecordsPausedNotice,\n} from '@aglyn/aglyn'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { Alert, AlertTitle, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport type { ReactNode } from 'react'\nimport ContactsCard from './contacts-card.component'\nimport { InboxCampaignsZone } from './inbox-attribution-zone'\nimport type { InboxConsoleSectionId } from './inbox-console-sections'\nimport SubmissionsCard from './submissions-card.component'\n\n/**\n * The body of one inbox section, built only when that section is the one being\n * read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT all three every render, and each card opens its Firestore\n * listens on mount — which is the entire cost this page exists to stop paying.\n * Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: InboxConsoleSectionId,\n hostId: string,\n): ReactNode {\n switch (section) {\n case 'submissions':\n return <SubmissionsCard hostId={hostId} />\n case 'contacts':\n return <ContactsCard hostId={hostId} />\n case 'campaigns':\n return <InboxCampaignsZone hostId={hostId} />\n default:\n return null\n }\n}\n\n/**\n * Inbox (AGL-77/104/109 → AGL-395): form submissions reader, site members +\n * leads, and campaigns — owned by the inbox plugin and rendered by the shell's\n * generic plugin route. The Campaigns section is a zone this page hosts; the\n * plugin that sends campaigns draws its card there.\n *\n * Orders are NOT here. A sale is not something that arrived in an inbox, and\n * the card was nested inside the members section rather than carrying a tab of\n * its own — so the rail listed three sections while the page drew a fourth\n * subject. Commerce already owns it: `commerce-console-sections.ts` declares\n * an `orders` section that renders the same card.\n *\n * Sections are ROUTES (AGL-2501), following the hubs that migrated before it.\n * Here it IS a read saving as well as an addressing one: this page carried\n * `HubTabs lazy`, which mounts one panel but keeps every panel it has visited,\n * so a reader who looked at Members & leads and went back to Submissions held\n * both sections' listens open for the rest of the visit. A URL per section\n * makes the saving structural — the page builds one section's body and the\n * others do not exist to subscribe. What routing adds besides is that the URL\n * names the section: it is linkable, the back button walks sections, and the\n * breadcrumb says where you are.\n */\nexport function InboxConsolePage(props: ConsolePluginPageProps) {\n const { hostId, section, sections, basePath } = props\n const firestore = useFirestore()\n\n // Submissions this site's abuse ceiling refused (AGL-1655 → AGL-1666).\n //\n // Until this, the refusal existed in two places a site owner cannot see: a\n // counters document only Firestore's console renders, and one in-app\n // notification that `system.` bucket-muting can suppress at write time —\n // `notifyUsers` skips the batch entirely, so a muted owner's notification\n // is never created and cannot be recovered by unmuting. This surface is\n // the durable one, and it is a plain read of the same document the\n // dropped-contacts alert uses (AGL-891).\n const { data: refusedCounter } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'counters', 'formSubmissionsRefused'),\n [firestore, hostId],\n )\n // Keyed by the month the SERVER wrote, via the shared helper — a key\n // derived differently here would read zero refusals on exactly the sites\n // being refused.\n const pausedNotice = formSubmissionsPausedNotice({\n refused: Number(refusedCounter?.[submissionMonthKey()] ?? 0),\n ceiling: Number(refusedCounter?.['ceiling']) || undefined,\n })\n\n // Bot submissions the honeypot caught (AGL-1831 → AGL-1836). The staff org\n // page has shown this number per host since AGL-1831; this is the same\n // count where the site's OWNER already looks, so \"is my form being hit by\n // bots?\" is answered by their own inbox instead of a support ticket. Same\n // client-unwritable counters document shape as the refusal counter above,\n // same host-admin read the rules already grant (AGL-1367), same shared\n // month key — and the shared sentence returns null below one catch, so a\n // quiet month renders nothing rather than a reassuring zero.\n const { data: spamCounter } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'counters', 'formSubmissionsSpam'),\n [firestore, hostId],\n )\n const spamNotice = formSpamCaughtNotice({\n spam: Number(spamCounter?.[submissionMonthKey()] ?? 0),\n })\n\n // Sign-ups and leads this site's PLATFORM ceiling refused (AGL-1529).\n //\n // Same instrument as the form ceiling directly above and read the same way:\n // a client-unwritable counters document (AGL-1367) that host admins can\n // already read, keyed by the SERVER's month through the shared helper — a\n // key derived differently here would read zero refusals on exactly the\n // sites being refused. The counter id comes from the shared function for\n // the same reason: the writer is in `@aglyn/tenant-data-admin` and this is\n // the reader, and two spellings of one document id is a surface that\n // renders nothing forever.\n //\n // This is the surface that makes the ceiling SHIPPED rather than merely\n // implemented. Without it a refusal exists in two places a site owner\n // cannot see: a Firestore document only our console renders, and one\n // notification that `system.` bucket-muting can suppress at write time.\n const { data: membersRefusedCounter } = useFirestoreDoc<any>(\n () =>\n doc(\n firestore,\n 'hosts',\n hostId,\n 'counters',\n visitorRecordRefusedCounterId('siteMembers'),\n ),\n [firestore, hostId],\n )\n const membersPausedNotice = visitorRecordsPausedNotice({\n kind: 'siteMembers',\n refused: Number(membersRefusedCounter?.[submissionMonthKey()] ?? 0),\n ceiling: Number(membersRefusedCounter?.['ceiling']) || undefined,\n })\n const { data: leadsRefusedCounter } = useFirestoreDoc<any>(\n () =>\n doc(\n firestore,\n 'hosts',\n hostId,\n 'counters',\n visitorRecordRefusedCounterId('leads'),\n ),\n [firestore, hostId],\n )\n const leadsPausedNotice = visitorRecordsPausedNotice({\n kind: 'leads',\n refused: Number(leadsRefusedCounter?.[submissionMonthKey()] ?? 0),\n ceiling: Number(leadsRefusedCounter?.['ceiling']) || undefined,\n })\n\n /*\n * Nothing until the URL names a section. The shell redirects a bare hub URL\n * to the landing section and holds a spinner while it does, so this state is\n * transient — and rendering a default section here instead would pay for its\n * listens on a URL that is already being replaced.\n *\n * The four counter reads above run either way, and that is deliberate: they\n * are four single-document listens, and hoisting them behind this guard\n * would make a ceiling notice appear a frame late on the section a reader\n * lands on.\n */\n if (!section || !sections?.length || !basePath) return null\n\n return (\n <>\n {/* Above the RAIL on purpose (AGL-1666). A paused form is not a fact\n about the Submissions section — it is why the whole inbox stopped\n filling — and the notification that brings an owner here links to\n the surface, not to a section. Inside one section's body it would be\n invisible to anyone who followed a link into another. */}\n {pausedNotice ? (\n <Alert severity=\"warning\" sx={{ mb: 2 }}>\n <AlertTitle>{pausedNotice.title}</AlertTitle>\n <Typography component=\"div\" variant=\"body2\">\n {pausedNotice.message}\n </Typography>\n <Typography\n component=\"div\"\n variant=\"body2\"\n color=\"text.secondary\"\n sx={{ mt: 0.5 }}\n >\n {pausedNotice.until}\n </Typography>\n </Alert>\n ) : null}\n {/* Beside the paused notice, and info rather than warning on purpose\n (AGL-1836): the honeypot count reports protection WORKING — those\n submissions were caught, dropped, and never stored or billed. An\n owner whose inbox went quiet checks here; bots absorbed silently is\n the answer that stops the support ticket. */}\n {spamNotice ? (\n <Alert severity=\"info\" sx={{ mb: 2 }}>\n {spamNotice}\n </Alert>\n ) : null}\n {/* Above the rail for the same reason the form notice is (AGL-1529):\n the Members and Leads lists are one section of several, and an owner\n who followed a link into another would never see a notice hidden\n inside that one. Two notices rather than one because the two ceilings\n are independent — a site can be refusing leads while sign-ups still\n land — and a merged sentence could only be true of both. Neither\n carries an `until` line: unlike the monthly form ceiling, these\n count LIVE DOCUMENTS and no date lifts them. */}\n {[membersPausedNotice, leadsPausedNotice].map((notice) =>\n notice ? (\n <Alert key={notice.title} severity=\"warning\" sx={{ mb: 2 }}>\n <AlertTitle>{notice.title}</AlertTitle>\n <Typography component=\"div\" variant=\"body2\">\n {notice.message}\n </Typography>\n </Alert>\n ) : null,\n )}\n <HubSections sections={sections}>\n {sectionBody(section as InboxConsoleSectionId, hostId)}\n </HubSections>\n </>\n )\n}\nInboxConsolePage.displayName = 'InboxConsolePage'\n\nexport default InboxConsolePage\n"],"names":["formSpamCaughtNotice","formSubmissionsPausedNotice","submissionMonthKey","visitorRecordRefusedCounterId","visitorRecordsPausedNotice","HubSections","useFirestore","useFirestoreDoc","Alert","AlertTitle","Typography","doc","ContactsCard","InboxCampaignsZone","SubmissionsCard","sectionBody","section","hostId","InboxConsolePage","props","sections","basePath","firestore","data","refusedCounter","pausedNotice","refused","Number","ceiling","undefined","spamCounter","spamNotice","spam","membersRefusedCounter","membersPausedNotice","kind","leadsRefusedCounter","leadsPausedNotice","length","severity","sx","mb","title","component","variant","message","color","mt","until","map","notice","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsolePluginOrgMount,\n type ConsolePluginPageProps,\n formSpamCaughtNotice,\n formSubmissionsPausedNotice,\n submissionMonthKey,\n visitorRecordRefusedCounterId,\n visitorRecordsPausedNotice,\n} from '@aglyn/aglyn'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n AlertTitle,\n Box,\n CircularProgress,\n MenuItem,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport type { ReactNode } from 'react'\nimport ContactsCard from './contacts-card.component'\nimport { InboxCampaignsZone } from './inbox-attribution-zone'\nimport type { InboxConsoleSectionId } from './inbox-console-sections'\nimport SubmissionsCard from './submissions-card.component'\nimport { useInboxSitePick } from './use-inbox-site-pick'\n\n/**\n * The body of one inbox section, built only when that section is the one being\n * read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT all three every render, and each card opens its Firestore\n * listens on mount — which is the entire cost this page exists to stop paying.\n * Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: InboxConsoleSectionId,\n hostId: string,\n): ReactNode {\n switch (section) {\n case 'submissions':\n return <SubmissionsCard hostId={hostId} />\n case 'contacts':\n return <ContactsCard hostId={hostId} />\n case 'campaigns':\n return <InboxCampaignsZone hostId={hostId} />\n default:\n return null\n }\n}\n\n/**\n * The body of one section on the ORGANIZATION's Inbox (AGL-3303): every\n * site's, with no site picked — the submissions in one collection-group\n * list, the organization's leads, every campaign — and exactly the site\n * view, through the same cards, once one is.\n *\n * Handing a picked site to the site body is the point rather than a\n * shortcut: that site's form filter, its members and its campaigns are\n * facts only a site has, and the cards that already read them are the ones\n * that read them correctly.\n */\nfunction orgSectionBody(\n section: InboxConsoleSectionId,\n mount: ConsolePluginOrgMount,\n siteHostId: string | null,\n): ReactNode {\n if (siteHostId) return sectionBody(section, siteHostId)\n switch (section) {\n case 'submissions':\n return <SubmissionsCard hostId={null} orgMount={mount} />\n case 'contacts':\n return <ContactsCard hostId={null} orgMount={mount} />\n case 'campaigns':\n return <InboxCampaignsZone hostId={null} orgMount={mount} />\n default:\n return null\n }\n}\n\n/**\n * Inbox (AGL-77/104/109 → AGL-395): form submissions reader, site members +\n * leads, and campaigns — owned by the inbox plugin and rendered by the shell's\n * generic plugin route. The Campaigns section is a zone this page hosts; the\n * plugin that sends campaigns draws its card there.\n *\n * Orders are NOT here. A sale is not something that arrived in an inbox, and\n * the card was nested inside the members section rather than carrying a tab of\n * its own — so the rail listed three sections while the page drew a fourth\n * subject. Commerce already owns it: `commerce-console-sections.ts` declares\n * an `orders` section that renders the same card.\n *\n * Sections are ROUTES (AGL-2501), following the hubs that migrated before it.\n * Here it IS a read saving as well as an addressing one: this page carried\n * `HubTabs lazy`, which mounts one panel but keeps every panel it has visited,\n * so a reader who looked at Members & leads and went back to Submissions held\n * both sections' listens open for the rest of the visit. A URL per section\n * makes the saving structural — the page builds one section's body and the\n * others do not exist to subscribe. What routing adds besides is that the URL\n * names the section: it is linkable, the back button walks sections, and the\n * breadcrumb says where you are.\n *\n * The ORGANIZATION's Inbox is this page handed no site (AGL-3303): the same\n * sections over every site at once, and a site filter above the rail that\n * narrows the whole page to one site's own view.\n */\nexport function InboxConsolePage(props: ConsolePluginPageProps) {\n const { hostId, orgMount, section, sections, basePath } = props\n const firestore = useFirestore()\n /*\n * On the organization's Inbox, the site the page is narrowed to — `null`\n * for every site (AGL-3303). Under a site there is no mount and no pick.\n */\n const sitePick = useInboxSitePick(hostId == null ? orgMount : undefined)\n /*\n * The site whose ceilings the notices below read: this page's own, or the\n * one picked on the organization's Inbox. With every site listed there is\n * no one site to read them for, and reading every site's four counters\n * would be a listener per site on a page that promises a bounded read, so\n * the notices wait for a pick.\n */\n const noticeHostId = hostId ?? sitePick.hostId\n\n // Submissions this site's abuse ceiling refused (AGL-1655 → AGL-1666).\n //\n // Until this, the refusal existed in two places a site owner cannot see: a\n // counters document only Firestore's console renders, and one in-app\n // notification that `system.` bucket-muting can suppress at write time —\n // `notifyUsers` skips the batch entirely, so a muted owner's notification\n // is never created and cannot be recovered by unmuting. This surface is\n // the durable one, and it is a plain read of the same document the\n // dropped-contacts alert uses (AGL-891).\n const { data: refusedCounter } = useFirestoreDoc<any>(\n () =>\n noticeHostId\n ? doc(\n firestore,\n 'hosts',\n noticeHostId,\n 'counters',\n 'formSubmissionsRefused',\n )\n : null,\n [firestore, noticeHostId],\n )\n // Keyed by the month the SERVER wrote, via the shared helper — a key\n // derived differently here would read zero refusals on exactly the sites\n // being refused.\n const pausedNotice = formSubmissionsPausedNotice({\n refused: Number(refusedCounter?.[submissionMonthKey()] ?? 0),\n ceiling: Number(refusedCounter?.['ceiling']) || undefined,\n })\n\n // Bot submissions the honeypot caught (AGL-1831 → AGL-1836). The staff org\n // page has shown this number per host since AGL-1831; this is the same\n // count where the site's OWNER already looks, so \"is my form being hit by\n // bots?\" is answered by their own inbox instead of a support ticket. Same\n // client-unwritable counters document shape as the refusal counter above,\n // same host-admin read the rules already grant (AGL-1367), same shared\n // month key — and the shared sentence returns null below one catch, so a\n // quiet month renders nothing rather than a reassuring zero.\n const { data: spamCounter } = useFirestoreDoc<any>(\n () =>\n noticeHostId\n ? doc(\n firestore,\n 'hosts',\n noticeHostId,\n 'counters',\n 'formSubmissionsSpam',\n )\n : null,\n [firestore, noticeHostId],\n )\n const spamNotice = formSpamCaughtNotice({\n spam: Number(spamCounter?.[submissionMonthKey()] ?? 0),\n })\n\n // Sign-ups and leads this site's PLATFORM ceiling refused (AGL-1529).\n //\n // Same instrument as the form ceiling directly above and read the same way:\n // a client-unwritable counters document (AGL-1367) that host admins can\n // already read, keyed by the SERVER's month through the shared helper — a\n // key derived differently here would read zero refusals on exactly the\n // sites being refused. The counter id comes from the shared function for\n // the same reason: the writer is in `@aglyn/tenant-data-admin` and this is\n // the reader, and two spellings of one document id is a surface that\n // renders nothing forever.\n //\n // This is the surface that makes the ceiling SHIPPED rather than merely\n // implemented. Without it a refusal exists in two places a site owner\n // cannot see: a Firestore document only our console renders, and one\n // notification that `system.` bucket-muting can suppress at write time.\n const { data: membersRefusedCounter } = useFirestoreDoc<any>(\n () =>\n noticeHostId\n ? doc(\n firestore,\n 'hosts',\n noticeHostId,\n 'counters',\n visitorRecordRefusedCounterId('siteMembers'),\n )\n : null,\n [firestore, noticeHostId],\n )\n const membersPausedNotice = visitorRecordsPausedNotice({\n kind: 'siteMembers',\n refused: Number(membersRefusedCounter?.[submissionMonthKey()] ?? 0),\n ceiling: Number(membersRefusedCounter?.['ceiling']) || undefined,\n })\n const { data: leadsRefusedCounter } = useFirestoreDoc<any>(\n () =>\n noticeHostId\n ? doc(\n firestore,\n 'hosts',\n noticeHostId,\n 'counters',\n visitorRecordRefusedCounterId('leads'),\n )\n : null,\n [firestore, noticeHostId],\n )\n const leadsPausedNotice = visitorRecordsPausedNotice({\n kind: 'leads',\n refused: Number(leadsRefusedCounter?.[submissionMonthKey()] ?? 0),\n ceiling: Number(leadsRefusedCounter?.['ceiling']) || undefined,\n })\n\n /*\n * Nothing until the URL names a section. The shell redirects a bare hub URL\n * to the landing section and holds a spinner while it does, so this state is\n * transient — and rendering a default section here instead would pay for its\n * listens on a URL that is already being replaced.\n *\n * The four counter reads above run either way, and that is deliberate: they\n * are four single-document listens, and hoisting them behind this guard\n * would make a ceiling notice appear a frame late on the section a reader\n * lands on. (On the organization's Inbox they run once a site is picked.)\n */\n if (!section || !sections?.length || !basePath) return null\n\n const notices = (\n <>\n {/* Above the RAIL on purpose (AGL-1666). A paused form is not a fact\n about the Submissions section — it is why the whole inbox stopped\n filling — and the notification that brings an owner here links to\n the surface, not to a section. Inside one section's body it would be\n invisible to anyone who followed a link into another. */}\n {pausedNotice ? (\n <Alert severity=\"warning\" sx={{ mb: 2 }}>\n <AlertTitle>{pausedNotice.title}</AlertTitle>\n <Typography component=\"div\" variant=\"body2\">\n {pausedNotice.message}\n </Typography>\n <Typography\n component=\"div\"\n variant=\"body2\"\n color=\"text.secondary\"\n sx={{ mt: 0.5 }}\n >\n {pausedNotice.until}\n </Typography>\n </Alert>\n ) : null}\n {/* Beside the paused notice, and info rather than warning on purpose\n (AGL-1836): the honeypot count reports protection WORKING — those\n submissions were caught, dropped, and never stored or billed. An\n owner whose inbox went quiet checks here; bots absorbed silently is\n the answer that stops the support ticket. */}\n {spamNotice ? (\n <Alert severity=\"info\" sx={{ mb: 2 }}>\n {spamNotice}\n </Alert>\n ) : null}\n {/* Above the rail for the same reason the form notice is (AGL-1529):\n the Members and Leads lists are one section of several, and an owner\n who followed a link into another would never see a notice hidden\n inside that one. Two notices rather than one because the two ceilings\n are independent — a site can be refusing leads while sign-ups still\n land — and a merged sentence could only be true of both. Neither\n carries an `until` line: unlike the monthly form ceiling, these\n count LIVE DOCUMENTS and no date lifts them. */}\n {[membersPausedNotice, leadsPausedNotice].map((notice) =>\n notice ? (\n <Alert key={notice.title} severity=\"warning\" sx={{ mb: 2 }}>\n <AlertTitle>{notice.title}</AlertTitle>\n <Typography component=\"div\" variant=\"body2\">\n {notice.message}\n </Typography>\n </Alert>\n ) : null,\n )}\n </>\n )\n\n if (hostId == null) {\n // No site and no org to stand in for it: nothing this page can scope.\n if (!orgMount) return null\n return (\n <>\n {/*\n The site filter sits above the rail with the notices, because it\n is a fact about the whole page: every section follows it, and the\n notices it brings in are the picked site's. An organization with\n one site has nothing to choose, and gets that site's page.\n */}\n {sitePick.options.length ? (\n <TextField\n select\n size=\"small\"\n label={'Site'}\n value={sitePick.hostId ?? ''}\n onChange={(event) => sitePick.setHostId(event.target.value || null)}\n // \"All sites\" is the empty value, which a select would otherwise\n // draw as a blank rather than as the choice it is — and the label\n // is shrunk with it, or it paints over that choice.\n slotProps={{\n select: { displayEmpty: true },\n inputLabel: { shrink: true },\n }}\n sx={{ mb: 2, minWidth: 240 }}\n >\n <MenuItem value=\"\">{'All sites'}</MenuItem>\n {sitePick.options.map((option) => (\n <MenuItem key={option.value} value={option.value}>\n {option.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {notices}\n <HubSections sections={sections}>\n {sitePick.ready ? (\n orgSectionBody(\n section as InboxConsoleSectionId,\n orgMount,\n sitePick.hostId,\n )\n ) : (\n <Box sx={{ p: 2 }}>\n <CircularProgress size={24} />\n </Box>\n )}\n </HubSections>\n </>\n )\n }\n\n return (\n <>\n {notices}\n <HubSections sections={sections}>\n {sectionBody(section as InboxConsoleSectionId, hostId)}\n </HubSections>\n </>\n )\n}\nInboxConsolePage.displayName = 'InboxConsolePage'\n\nexport default InboxConsolePage\n"],"names":["formSpamCaughtNotice","formSubmissionsPausedNotice","submissionMonthKey","visitorRecordRefusedCounterId","visitorRecordsPausedNotice","HubSections","useFirestore","useFirestoreDoc","Alert","AlertTitle","Box","CircularProgress","MenuItem","TextField","Typography","doc","ContactsCard","InboxCampaignsZone","SubmissionsCard","useInboxSitePick","sectionBody","section","hostId","orgSectionBody","mount","siteHostId","orgMount","InboxConsolePage","props","sections","basePath","firestore","sitePick","undefined","noticeHostId","data","refusedCounter","pausedNotice","refused","Number","ceiling","spamCounter","spamNotice","spam","membersRefusedCounter","membersPausedNotice","kind","leadsRefusedCounter","leadsPausedNotice","length","notices","severity","sx","mb","title","component","variant","message","color","mt","until","map","notice","options","select","size","label","value","onChange","event","setHostId","target","slotProps","displayEmpty","inputLabel","shrink","minWidth","option","ready","p","displayName"],"mappings":"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"}
|
|
@@ -38,3 +38,16 @@ export type InboxConsoleSectionId = 'submissions' | 'contacts' | 'campaigns';
|
|
|
38
38
|
* inherit the Inbox nav item's gate.
|
|
39
39
|
*/
|
|
40
40
|
export declare const INBOX_CONSOLE_SECTIONS: readonly ConsoleNavSection[];
|
|
41
|
+
/**
|
|
42
|
+
* The ORGANIZATION's Inbox sections, at `/[orgSlug]/inbox` (AGL-3303): the
|
|
43
|
+
* same three, read off the list above so a section keeps one id and one
|
|
44
|
+
* label at both levels.
|
|
45
|
+
*
|
|
46
|
+
* Members & leads asks for `data.manage` here and not on the site rail.
|
|
47
|
+
* Without a site it lists the organization's leads unscoped, and the rules
|
|
48
|
+
* admit that read on the authority the CRM's own gate names
|
|
49
|
+
* (`canReadScopedPeople`); a reader without it would be offered a table that
|
|
50
|
+
* is refused. Under a site the same section still lists the site's members,
|
|
51
|
+
* which nobody's permission narrows.
|
|
52
|
+
*/
|
|
53
|
+
export declare const INBOX_ORG_CONSOLE_SECTIONS: readonly ConsoleNavSection[];
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
/**
|
|
17
18
|
* The inbox console's sections, in rail order (AGL-2501).
|
|
18
19
|
*
|
|
19
20
|
* One list, read twice and never copied: `plugin.ts` registers it on the nav
|
|
@@ -56,11 +57,25 @@
|
|
|
56
57
|
id: 'campaigns',
|
|
57
58
|
label: 'Campaigns'
|
|
58
59
|
}
|
|
59
|
-
]
|
|
60
|
+
];
|
|
61
|
+
/*
|
|
60
62
|
* Rail ORDER decides where `/inbox` lands: the shell redirects a bare hub URL
|
|
61
63
|
* to the first section in this list the reader may open (AGL-2501). There is
|
|
62
64
|
* deliberately no separate default constant — a second place to say which
|
|
63
65
|
* section is first is a second place for it to disagree with the rail.
|
|
64
|
-
*/
|
|
66
|
+
*/ /**
|
|
67
|
+
* The ORGANIZATION's Inbox sections, at `/[orgSlug]/inbox` (AGL-3303): the
|
|
68
|
+
* same three, read off the list above so a section keeps one id and one
|
|
69
|
+
* label at both levels.
|
|
70
|
+
*
|
|
71
|
+
* Members & leads asks for `data.manage` here and not on the site rail.
|
|
72
|
+
* Without a site it lists the organization's leads unscoped, and the rules
|
|
73
|
+
* admit that read on the authority the CRM's own gate names
|
|
74
|
+
* (`canReadScopedPeople`); a reader without it would be offered a table that
|
|
75
|
+
* is refused. Under a site the same section still lists the site's members,
|
|
76
|
+
* which nobody's permission narrows.
|
|
77
|
+
*/ export const INBOX_ORG_CONSOLE_SECTIONS = INBOX_CONSOLE_SECTIONS.map((section)=>section.id === 'contacts' ? _extends({}, section, {
|
|
78
|
+
permission: 'data.manage'
|
|
79
|
+
}) : section);
|
|
65
80
|
|
|
66
81
|
//# sourceMappingURL=inbox-console-sections.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-console-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type InboxConsoleSectionId = 'submissions' | 'contacts' | 'campaigns'\n\n/**\n * The inbox console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back. A second copy is how a section comes\n * to be routable under one id and drawn under another.\n *\n * Ids are the `?tab=` ids this page already deep-linked by, kept deliberately\n * so a bookmark that named a tab names the same section as a route. `contacts`\n * therefore keeps its id while its LABEL reads \"Members & leads\".\n *\n * ORDERS ARE NOT HERE, and the omission is the decision. A sale is not\n * something that arrived in an inbox, and commerce owns the surface that\n * lists them: `commerce-console-sections.ts` declares an `orders` section\n * rendering the same card. Adding one here would give one record two\n * addresses.\n *\n * No `navTabId` on any of them: every section ships with the surface, so they\n * inherit the Inbox nav item's gate.\n */\nexport const INBOX_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'submissions', label: 'Submissions' },\n /*\n * The people a site collected, members and leads in one list. Its label\n * names both because the table is one list of two collections — a member\n * who was a lead first appears once, and a rail reading \"Contacts\" beside\n * the Contacts nav item would name a different surface's subject.\n */\n { id: 'contacts', label: 'Members & leads' },\n /*\n * Borrowed from the marketing plugin, which owns the campaign. It is listed\n * here because a campaign is the outbound half of the same conversation the\n * submissions are the inbound half of, and a merchant reading one reaches\n * for the other.\n */\n { id: 'campaigns', label: 'Campaigns' },\n]\n\n/*\n * Rail ORDER decides where `/inbox` lands: the shell redirects a bare hub URL\n * to the first section in this list the reader may open (AGL-2501). There is\n * deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n"],"names":["INBOX_CONSOLE_SECTIONS","id","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-console-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type InboxConsoleSectionId = 'submissions' | 'contacts' | 'campaigns'\n\n/**\n * The inbox console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back. A second copy is how a section comes\n * to be routable under one id and drawn under another.\n *\n * Ids are the `?tab=` ids this page already deep-linked by, kept deliberately\n * so a bookmark that named a tab names the same section as a route. `contacts`\n * therefore keeps its id while its LABEL reads \"Members & leads\".\n *\n * ORDERS ARE NOT HERE, and the omission is the decision. A sale is not\n * something that arrived in an inbox, and commerce owns the surface that\n * lists them: `commerce-console-sections.ts` declares an `orders` section\n * rendering the same card. Adding one here would give one record two\n * addresses.\n *\n * No `navTabId` on any of them: every section ships with the surface, so they\n * inherit the Inbox nav item's gate.\n */\nexport const INBOX_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'submissions', label: 'Submissions' },\n /*\n * The people a site collected, members and leads in one list. Its label\n * names both because the table is one list of two collections — a member\n * who was a lead first appears once, and a rail reading \"Contacts\" beside\n * the Contacts nav item would name a different surface's subject.\n */\n { id: 'contacts', label: 'Members & leads' },\n /*\n * Borrowed from the marketing plugin, which owns the campaign. It is listed\n * here because a campaign is the outbound half of the same conversation the\n * submissions are the inbound half of, and a merchant reading one reaches\n * for the other.\n */\n { id: 'campaigns', label: 'Campaigns' },\n]\n\n/*\n * Rail ORDER decides where `/inbox` lands: the shell redirects a bare hub URL\n * to the first section in this list the reader may open (AGL-2501). There is\n * deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n\n/**\n * The ORGANIZATION's Inbox sections, at `/[orgSlug]/inbox` (AGL-3303): the\n * same three, read off the list above so a section keeps one id and one\n * label at both levels.\n *\n * Members & leads asks for `data.manage` here and not on the site rail.\n * Without a site it lists the organization's leads unscoped, and the rules\n * admit that read on the authority the CRM's own gate names\n * (`canReadScopedPeople`); a reader without it would be offered a table that\n * is refused. Under a site the same section still lists the site's members,\n * which nobody's permission narrows.\n */\nexport const INBOX_ORG_CONSOLE_SECTIONS: readonly ConsoleNavSection[] =\n INBOX_CONSOLE_SECTIONS.map((section) =>\n section.id === 'contacts'\n ? { ...section, permission: 'data.manage' }\n : section,\n )\n"],"names":["INBOX_CONSOLE_SECTIONS","id","label","INBOX_ORG_CONSOLE_SECTIONS","map","section","permission"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAMD;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMA,yBAAuD;IAClE;QAAEC,IAAI;QAAeC,OAAO;IAAc;IAC1C;;;;;GAKC,GACD;QAAED,IAAI;QAAYC,OAAO;IAAkB;IAC3C;;;;;GAKC,GACD;QAAED,IAAI;QAAaC,OAAO;IAAY;CACvC,CAAA;AAED;;;;;CAKC,GAED;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,6BACXH,uBAAuBI,GAAG,CAAC,CAACC,UAC1BA,QAAQJ,EAAE,KAAK,aACX,aAAKI;QAASC,YAAY;SAC1BD,SACL"}
|
|
@@ -83,6 +83,10 @@ import { scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
|
83
83
|
* The lead silo is the org's (AGL-3275), narrowed to this site by
|
|
84
84
|
* `visibleTo`. Reading `hosts/{hostId}/leads` showed a site its
|
|
85
85
|
* pre-migration rows and nothing captured since.
|
|
86
|
+
*
|
|
87
|
+
* `orgId` settles AFTER the first render — it is a `hostIndex` lookup —
|
|
88
|
+
* so it is a dependency of the counts below: an effect keyed on the site
|
|
89
|
+
* alone ran once with no org, returned, and never asked again.
|
|
86
90
|
*/ const { orgId } = useOrgDataScope({
|
|
87
91
|
hostId
|
|
88
92
|
});
|
|
@@ -104,7 +108,8 @@ import { scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
|
104
108
|
};
|
|
105
109
|
}, [
|
|
106
110
|
firestore,
|
|
107
|
-
hostId
|
|
111
|
+
hostId,
|
|
112
|
+
orgId
|
|
108
113
|
]);
|
|
109
114
|
const submissions = submissionDocs != null ? submissionDocs : [];
|
|
110
115
|
if (!submissions.length && !openLeads) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-glance-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n buildRoute,\n CRM_LEAD_CLOSED_STATUSES,\n openLeadsFromCounts,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\n// The CRM's route builder by its leaf path, not the plugin barrel: the\n// barrel is the CRM's site entry point, and a dashboard card named there\n// would ship to every published page.\nimport { pluginRecordListHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Avatar, Box, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n getCountFromServer,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useParams } from 'next/navigation'\nimport { useEffect, useState } from 'react'\nimport { useOrgDataScope, useFirestore, useFirestoreCollection } from '@aglyn/tenant-feature-instance'\nimport { useRecordRouteContext } from './use-record-route-context'\nimport {\n relativeTime,\n senderHue,\n submissionSender,\n} from '../model/submission-presenter'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\n\n/**\n * Rows on the card, plus one so it can say the inbox holds more.\n *\n * The extra document is never rendered — it is what turns \"there may be\n * more\" into a fact, the same probe the console's paged lists use.\n */\nconst PREVIEW_ROWS = 3\n\n/**\n * The inbox at a glance: who wrote in, how long ago, and how many are\n * unread.\n *\n * A dashboard is where a site owner looks first, and a form submission is\n * the one thing on it that is waiting for a REPLY — until now it was two\n * clicks away with nothing on the dashboard saying it had arrived.\n *\n * Renders nothing until the site has a submission, the same rule the other\n * capability glances follow: an empty card about forms on a site with no\n * form is an advertisement, not a summary.\n *\n * `orderBy('createdAt', 'desc')` is checked against the writer rather than\n * assumed — an `orderBy` drops documents missing the field, which on a\n * newest-first list is invisible. `apps/tenant/app/api/forms/submit/route.ts`\n * is the only path that creates one and stamps `createdAt: serverTimestamp()`\n * on every add, the v1 API only reads and deletes, and `formSubmissions` is\n * absent from `IMPORTABLE_FIELDS`, so no restore can mint one without it.\n */\nexport function InboxGlanceCard(props: { hostId: string }) {\n const { hostId } = props\n const firestore = useFirestore()\n const { orgSlug, host } = useParams<{ orgSlug: string; host: string }>()\n const { data: submissionDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'formSubmissions'),\n orderBy('createdAt', 'desc'),\n limit(PREVIEW_ROWS + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n\n /*\n * How many leads are waiting to be worked (AGL-2622), so the dashboard\n * says where the Inbox's captures went and the CRM's Leads list is one\n * click from here. Two server-side counts rather than a window: a lead\n * nobody has touched carries no status field at all — it is `new` by\n * absence, the rule `crmLeadStatus` reads — and Firestore can neither\n * count an absent field nor exclude the closed statuses without dropping\n * the unstamped rows too. So the open count is every lead less the ones\n * closed by a stamped status — `openLeadsFromCounts`, the subtraction the\n * CRM's own glance card makes — and both counts are aggregations that cost\n * one read per thousand index entries, not one per lead. `null` until\n * they land, and left null on a refused read so the line is withheld\n * rather than drawn as zero.\n */\n const routeContext = useRecordRouteContext()\n const leadsHref = routeContext\n ? pluginRecordListHref('lead', routeContext)\n : null\n /*\n * The lead silo is the org's (AGL-3275), narrowed to this site by\n * `visibleTo`. Reading `hosts/{hostId}/leads` showed a site its\n * pre-migration rows and nothing captured since.\n */\n const { orgId } = useOrgDataScope({ hostId })\n const [openLeads, setOpenLeads] = useState<number | null>(null)\n useEffect(() => {\n let active = true\n if (!orgId) return undefined\n const leads = collection(firestore, 'orgs', orgId, 'leads')\n const scoped = where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId))\n void Promise.all([\n getCountFromServer(query(leads, scoped)),\n getCountFromServer(\n query(leads, scoped, where('status', 'in', CRM_LEAD_CLOSED_STATUSES)),\n ),\n ])\n .then(([all, closed]) => {\n if (!active) return\n setOpenLeads(openLeadsFromCounts(all.data().count, closed.data().count))\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId])\n\n const submissions = submissionDocs ?? []\n if (!submissions.length && !openLeads) return null\n const rows = submissions.slice(0, PREVIEW_ROWS)\n const unread = rows.filter((submission: any) => !submission.read).length\n\n return (\n <CardDisplay\n header={'Inbox'}\n help={pluginDocsHelp('forms', {\n anchor: '#the-inbox',\n excerpt:\n 'The newest form submissions on this site — the Inbox has the ' +\n 'full list, the reader, and the routing each one took.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={buildRoute(Route.HOST_INBOX, { orgSlug, host })}\n size=\"small\"\n color=\"primary\"\n >\n {'Open inbox'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1}>\n {rows.map((submission: any) => {\n const sender = submissionSender(submission.fields)\n const hue = senderHue(sender.label)\n return (\n <Stack\n key={submission.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n {/*\n The unread DOT, as the Inbox itself draws it — same fact, same\n shape, so a reader recognizes the row when they open it.\n */}\n <Box\n aria-label={submission.read ? undefined : 'Unread'}\n sx={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n bgcolor: submission.read ? 'transparent' : 'primary.main',\n flexShrink: 0,\n }}\n />\n <Avatar\n sx={{\n width: 28,\n height: 28,\n typography: 'caption',\n bgcolor: `hsl(${hue} 55% 45%)`,\n }}\n >\n {sender.initials}\n </Avatar>\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {sender.label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {submission.formName ?? 'Form'}\n </Typography>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {relativeTime(submission.createdAt?.toDate?.().getTime())}\n </Typography>\n </Stack>\n )\n })}\n {/*\n What the card is NOT showing. `submissions` holds the probe row, so\n a fourth document is a fact rather than an inference from a full\n page — and the unread count is stated over the rows on screen, not\n over the collection, because counting the collection is a read this\n card has no reason to pay for.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n unread ? `${unread} unread here` : null,\n submissions.length > PREVIEW_ROWS ? 'more in the Inbox' : null,\n ]\n .filter(Boolean)\n .join(' · ') || 'All caught up'}\n </Typography>\n {/*\n Where the captures went (AGL-2622): the leads still to be worked,\n and the Leads list where they are, which the plugin that keeps\n them publishes. Text alone until the route params settle or where\n no plugin publishes one, so the line never links to nowhere.\n */}\n {openLeads ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${openLeads.toLocaleString()} open lead${openLeads === 1 ? '' : 's'} · `}\n {leadsHref ? (\n <AppLink href={leadsHref}>\n {'Work them in the CRM'}\n </AppLink>\n ) : (\n 'in the CRM'\n )}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nInboxGlanceCard.displayName = 'InboxGlanceCard'\n\nexport default InboxGlanceCard\n"],"names":["buildRoute","CRM_LEAD_CLOSED_STATUSES","openLeadsFromCounts","pluginDocsHelp","Route","pluginRecordListHref","AppLink","CardDisplay","Avatar","Box","Button","Stack","Typography","collection","getCountFromServer","limit","orderBy","query","where","useParams","useEffect","useState","useOrgDataScope","useFirestore","useFirestoreCollection","useRecordRouteContext","relativeTime","senderHue","submissionSender","scopeTokensForHost","PREVIEW_ROWS","InboxGlanceCard","props","hostId","firestore","orgSlug","host","data","submissionDocs","idField","routeContext","leadsHref","orgId","openLeads","setOpenLeads","active","undefined","leads","scoped","Promise","all","then","closed","count","catch","submissions","length","rows","slice","unread","filter","submission","read","header","help","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","component","componentVariant","nativeButton","href","HOST_INBOX","size","color","spacing","map","sender","fields","hue","label","direction","sx","alignItems","aria-label","width","height","borderRadius","bgcolor","flexShrink","typography","initials","flex","minWidth","variant","noWrap","formName","createdAt","toDate","getTime","$id","Boolean","join","toLocaleString","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-glance-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n buildRoute,\n CRM_LEAD_CLOSED_STATUSES,\n openLeadsFromCounts,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\n// The CRM's route builder by its leaf path, not the plugin barrel: the\n// barrel is the CRM's site entry point, and a dashboard card named there\n// would ship to every published page.\nimport { pluginRecordListHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Avatar, Box, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n getCountFromServer,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useParams } from 'next/navigation'\nimport { useEffect, useState } from 'react'\nimport { useOrgDataScope, useFirestore, useFirestoreCollection } from '@aglyn/tenant-feature-instance'\nimport { useRecordRouteContext } from './use-record-route-context'\nimport {\n relativeTime,\n senderHue,\n submissionSender,\n} from '../model/submission-presenter'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\n\n/**\n * Rows on the card, plus one so it can say the inbox holds more.\n *\n * The extra document is never rendered — it is what turns \"there may be\n * more\" into a fact, the same probe the console's paged lists use.\n */\nconst PREVIEW_ROWS = 3\n\n/**\n * The inbox at a glance: who wrote in, how long ago, and how many are\n * unread.\n *\n * A dashboard is where a site owner looks first, and a form submission is\n * the one thing on it that is waiting for a REPLY — until now it was two\n * clicks away with nothing on the dashboard saying it had arrived.\n *\n * Renders nothing until the site has a submission, the same rule the other\n * capability glances follow: an empty card about forms on a site with no\n * form is an advertisement, not a summary.\n *\n * `orderBy('createdAt', 'desc')` is checked against the writer rather than\n * assumed — an `orderBy` drops documents missing the field, which on a\n * newest-first list is invisible. `apps/tenant/app/api/forms/submit/route.ts`\n * is the only path that creates one and stamps `createdAt: serverTimestamp()`\n * on every add, the v1 API only reads and deletes, and `formSubmissions` is\n * absent from `IMPORTABLE_FIELDS`, so no restore can mint one without it.\n */\nexport function InboxGlanceCard(props: { hostId: string }) {\n const { hostId } = props\n const firestore = useFirestore()\n const { orgSlug, host } = useParams<{ orgSlug: string; host: string }>()\n const { data: submissionDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'formSubmissions'),\n orderBy('createdAt', 'desc'),\n limit(PREVIEW_ROWS + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n\n /*\n * How many leads are waiting to be worked (AGL-2622), so the dashboard\n * says where the Inbox's captures went and the CRM's Leads list is one\n * click from here. Two server-side counts rather than a window: a lead\n * nobody has touched carries no status field at all — it is `new` by\n * absence, the rule `crmLeadStatus` reads — and Firestore can neither\n * count an absent field nor exclude the closed statuses without dropping\n * the unstamped rows too. So the open count is every lead less the ones\n * closed by a stamped status — `openLeadsFromCounts`, the subtraction the\n * CRM's own glance card makes — and both counts are aggregations that cost\n * one read per thousand index entries, not one per lead. `null` until\n * they land, and left null on a refused read so the line is withheld\n * rather than drawn as zero.\n */\n const routeContext = useRecordRouteContext()\n const leadsHref = routeContext\n ? pluginRecordListHref('lead', routeContext)\n : null\n /*\n * The lead silo is the org's (AGL-3275), narrowed to this site by\n * `visibleTo`. Reading `hosts/{hostId}/leads` showed a site its\n * pre-migration rows and nothing captured since.\n *\n * `orgId` settles AFTER the first render — it is a `hostIndex` lookup —\n * so it is a dependency of the counts below: an effect keyed on the site\n * alone ran once with no org, returned, and never asked again.\n */\n const { orgId } = useOrgDataScope({ hostId })\n const [openLeads, setOpenLeads] = useState<number | null>(null)\n useEffect(() => {\n let active = true\n if (!orgId) return undefined\n const leads = collection(firestore, 'orgs', orgId, 'leads')\n const scoped = where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId))\n void Promise.all([\n getCountFromServer(query(leads, scoped)),\n getCountFromServer(\n query(leads, scoped, where('status', 'in', CRM_LEAD_CLOSED_STATUSES)),\n ),\n ])\n .then(([all, closed]) => {\n if (!active) return\n setOpenLeads(openLeadsFromCounts(all.data().count, closed.data().count))\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId, orgId])\n\n const submissions = submissionDocs ?? []\n if (!submissions.length && !openLeads) return null\n const rows = submissions.slice(0, PREVIEW_ROWS)\n const unread = rows.filter((submission: any) => !submission.read).length\n\n return (\n <CardDisplay\n header={'Inbox'}\n help={pluginDocsHelp('forms', {\n anchor: '#the-inbox',\n excerpt:\n 'The newest form submissions on this site — the Inbox has the ' +\n 'full list, the reader, and the routing each one took.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={buildRoute(Route.HOST_INBOX, { orgSlug, host })}\n size=\"small\"\n color=\"primary\"\n >\n {'Open inbox'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1}>\n {rows.map((submission: any) => {\n const sender = submissionSender(submission.fields)\n const hue = senderHue(sender.label)\n return (\n <Stack\n key={submission.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n {/*\n The unread DOT, as the Inbox itself draws it — same fact, same\n shape, so a reader recognizes the row when they open it.\n */}\n <Box\n aria-label={submission.read ? undefined : 'Unread'}\n sx={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n bgcolor: submission.read ? 'transparent' : 'primary.main',\n flexShrink: 0,\n }}\n />\n <Avatar\n sx={{\n width: 28,\n height: 28,\n typography: 'caption',\n bgcolor: `hsl(${hue} 55% 45%)`,\n }}\n >\n {sender.initials}\n </Avatar>\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {sender.label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {submission.formName ?? 'Form'}\n </Typography>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {relativeTime(submission.createdAt?.toDate?.().getTime())}\n </Typography>\n </Stack>\n )\n })}\n {/*\n What the card is NOT showing. `submissions` holds the probe row, so\n a fourth document is a fact rather than an inference from a full\n page — and the unread count is stated over the rows on screen, not\n over the collection, because counting the collection is a read this\n card has no reason to pay for.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n unread ? `${unread} unread here` : null,\n submissions.length > PREVIEW_ROWS ? 'more in the Inbox' : null,\n ]\n .filter(Boolean)\n .join(' · ') || 'All caught up'}\n </Typography>\n {/*\n Where the captures went (AGL-2622): the leads still to be worked,\n and the Leads list where they are, which the plugin that keeps\n them publishes. Text alone until the route params settle or where\n no plugin publishes one, so the line never links to nowhere.\n */}\n {openLeads ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${openLeads.toLocaleString()} open lead${openLeads === 1 ? '' : 's'} · `}\n {leadsHref ? (\n <AppLink href={leadsHref}>\n {'Work them in the CRM'}\n </AppLink>\n ) : (\n 'in the CRM'\n )}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nInboxGlanceCard.displayName = 'InboxGlanceCard'\n\nexport default InboxGlanceCard\n"],"names":["buildRoute","CRM_LEAD_CLOSED_STATUSES","openLeadsFromCounts","pluginDocsHelp","Route","pluginRecordListHref","AppLink","CardDisplay","Avatar","Box","Button","Stack","Typography","collection","getCountFromServer","limit","orderBy","query","where","useParams","useEffect","useState","useOrgDataScope","useFirestore","useFirestoreCollection","useRecordRouteContext","relativeTime","senderHue","submissionSender","scopeTokensForHost","PREVIEW_ROWS","InboxGlanceCard","props","hostId","firestore","orgSlug","host","data","submissionDocs","idField","routeContext","leadsHref","orgId","openLeads","setOpenLeads","active","undefined","leads","scoped","Promise","all","then","closed","count","catch","submissions","length","rows","slice","unread","filter","submission","read","header","help","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","component","componentVariant","nativeButton","href","HOST_INBOX","size","color","spacing","map","sender","fields","hue","label","direction","sx","alignItems","aria-label","width","height","borderRadius","bgcolor","flexShrink","typography","initials","flex","minWidth","variant","noWrap","formName","createdAt","toDate","getTime","$id","Boolean","join","toLocaleString","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SACEA,UAAU,EACVC,wBAAwB,EACxBC,mBAAmB,EACnBC,cAAc,EACdC,KAAK,QACA,eAAc;AACrB,uEAAuE;AACvE,yEAAyE;AACzE,sCAAsC;AACtC,SAASC,oBAAoB,QAAQ,mDAAkD;AACvF,SAASC,OAAO,EAAEC,WAAW,QAAQ,uBAAsB;AAC3D,SAASC,MAAM,EAAEC,GAAG,EAAEC,MAAM,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACtE,SACEC,UAAU,EACVC,kBAAkB,EAClBC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,KAAK,QACA,qBAAoB;AAC3B,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,eAAe,EAAEC,YAAY,EAAEC,sBAAsB,QAAQ,iCAAgC;AACtG,SAASC,qBAAqB,QAAQ,gCAA4B;AAClE,SACEC,YAAY,EACZC,SAAS,EACTC,gBAAgB,QACX,mCAA+B;AACtC,SAASC,kBAAkB,QAAQ,sCAAqC;AAExE;;;;;CAKC,GACD,MAAMC,eAAe;AAErB;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,gBAAgBC,KAAyB;IACvD,MAAM,EAAEC,MAAM,EAAE,GAAGD;IACnB,MAAME,YAAYX;IAClB,MAAM,EAAEY,OAAO,EAAEC,IAAI,EAAE,GAAGjB;IAC1B,MAAM,EAAEkB,MAAMC,cAAc,EAAE,GAAGd,uBAC/B,IACEP,MACEJ,WAAWqB,WAAW,SAASD,QAAQ,oBACvCjB,QAAQ,aAAa,SACrBD,MAAMe,eAAe,KAEzB;QAACI;QAAWD;KAAO,EACnB;QAAEM,SAAS;IAAM;IAGnB;;;;;;;;;;;;;GAaC,GACD,MAAMC,eAAef;IACrB,MAAMgB,YAAYD,eACdnC,qBAAqB,QAAQmC,gBAC7B;IACJ;;;;;;;;GAQC,GACD,MAAM,EAAEE,KAAK,EAAE,GAAGpB,gBAAgB;QAAEW;IAAO;IAC3C,MAAM,CAACU,WAAWC,aAAa,GAAGvB,SAAwB;IAC1DD,UAAU;QACR,IAAIyB,SAAS;QACb,IAAI,CAACH,OAAO,OAAOI;QACnB,MAAMC,QAAQlC,WAAWqB,WAAW,QAAQQ,OAAO;QACnD,MAAMM,SAAS9B,MAAM,aAAa,sBAAsBW,mBAAmBI;QAC3E,KAAKgB,QAAQC,GAAG,CAAC;YACfpC,mBAAmBG,MAAM8B,OAAOC;YAChClC,mBACEG,MAAM8B,OAAOC,QAAQ9B,MAAM,UAAU,MAAMjB;SAE9C,EACEkD,IAAI,CAAC,CAAC,CAACD,KAAKE,OAAO;YAClB,IAAI,CAACP,QAAQ;YACbD,aAAa1C,oBAAoBgD,IAAIb,IAAI,GAAGgB,KAAK,EAAED,OAAOf,IAAI,GAAGgB,KAAK;QACxE,GACCC,KAAK,CAAC,IAAMR;QACf,OAAO;YACLD,SAAS;QACX;IACF,GAAG;QAACX;QAAWD;QAAQS;KAAM;IAE7B,MAAMa,cAAcjB,yBAAAA,iBAAkB,EAAE;IACxC,IAAI,CAACiB,YAAYC,MAAM,IAAI,CAACb,WAAW,OAAO;IAC9C,MAAMc,OAAOF,YAAYG,KAAK,CAAC,GAAG5B;IAClC,MAAM6B,SAASF,KAAKG,MAAM,CAAC,CAACC,aAAoB,CAACA,WAAWC,IAAI,EAAEN,MAAM;IAExE,qBACE,KAACjD;QACCwD,QAAQ;QACRC,MAAM7D,eAAe,SAAS;YAC5B8D,QAAQ;YACRC,SACE,kEACA;QACJ;QACAC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,KAAC5D;gBACC6D,WAAWjE;eACN;gBAAEkE,kBAAkB;gBAASC,cAAc;YAAM;gBACtDC,MAAM1E,WAAWI,MAAMuE,UAAU,EAAE;oBAAExC;oBAASC;gBAAK;gBACnDwC,MAAK;gBACLC,OAAM;0BAEL;;QAGP;kBAEA,cAAA,MAAClE;YAAMmE,SAAS;;gBACbrB,KAAKsB,GAAG,CAAC,CAAClB;wBAuCAA;wBAIWA,8BAAAA;oBA1CpB,MAAMmB,SAASpD,iBAAiBiC,WAAWoB,MAAM;oBACjD,MAAMC,MAAMvD,UAAUqD,OAAOG,KAAK;oBAClC,qBACE,MAACxE;wBAECyE,WAAU;wBACVN,SAAS;wBACTO,IAAI;4BAAEC,YAAY;wBAAS;;0CAM3B,KAAC7E;gCACC8E,cAAY1B,WAAWC,IAAI,GAAGhB,YAAY;gCAC1CuC,IAAI;oCACFG,OAAO;oCACPC,QAAQ;oCACRC,cAAc;oCACdC,SAAS9B,WAAWC,IAAI,GAAG,gBAAgB;oCAC3C8B,YAAY;gCACd;;0CAEF,KAACpF;gCACC6E,IAAI;oCACFG,OAAO;oCACPC,QAAQ;oCACRI,YAAY;oCACZF,SAAS,CAAC,IAAI,EAAET,IAAI,SAAS,CAAC;gCAChC;0CAECF,OAAOc,QAAQ;;0CAElB,MAACnF;gCAAM0E,IAAI;oCAAEU,MAAM;oCAAGC,UAAU;gCAAE;;kDAChC,KAACpF;wCAAWqF,SAAQ;wCAAQC,MAAM;kDAC/BlB,OAAOG,KAAK;;kDAEf,KAACvE;wCAAWqF,SAAQ;wCAAUpB,OAAM;wCAAiBqB,MAAM;mDACxDrC,uBAAAA,WAAWsC,QAAQ,YAAnBtC,uBAAuB;;;;0CAG5B,KAACjD;gCAAWqF,SAAQ;gCAAUpB,OAAM;gCAAiBqB,MAAM;0CACxDxE,cAAamC,wBAAAA,WAAWuC,SAAS,sBAApBvC,+BAAAA,sBAAsBwC,MAAM,qBAA5BxC,kCAAAA,uBAAiCyC,OAAO;;;uBAtCnDzC,WAAW0C,GAAG;gBA0CzB;8BAQA,KAAC3F;oBAAWqF,SAAQ;oBAAUpB,OAAM;8BACjC;wBACClB,SAAS,GAAGA,OAAO,YAAY,CAAC,GAAG;wBACnCJ,YAAYC,MAAM,GAAG1B,eAAe,sBAAsB;qBAC3D,CACE8B,MAAM,CAAC4C,SACPC,IAAI,CAAC,UAAU;;gBAQnB9D,0BACC,MAAC/B;oBAAWqF,SAAQ;oBAAUpB,OAAM;;wBACjC,GAAGlC,UAAU+D,cAAc,GAAG,UAAU,EAAE/D,cAAc,IAAI,KAAK,IAAI,GAAG,CAAC;wBACzEF,0BACC,KAACnC;4BAAQoE,MAAMjC;sCACZ;6BAGH;;qBAGF;;;;AAIZ;AACAV,gBAAgB4E,WAAW,GAAG;AAE9B,eAAe5E,gBAAe"}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { ConsolePluginOrgMount } from '@aglyn/aglyn';
|
|
18
|
+
/**
|
|
19
|
+
* How the organization's Inbox names a site (AGL-3303).
|
|
20
|
+
*
|
|
21
|
+
* A row holds a host DOCUMENT ID and a person reads a NAME; the shell's mount
|
|
22
|
+
* carries both for every site the reader may open. A site the mount cannot
|
|
23
|
+
* name is shown by its id rather than left blank — the row is real either way.
|
|
24
|
+
*/
|
|
25
|
+
export declare function orgSiteName(mount: ConsolePluginOrgMount | null | undefined, hostId: unknown): string;
|
|
26
|
+
/** Every site a lead was captured by, by name, in capture order. */
|
|
27
|
+
export declare function orgSiteNames(mount: ConsolePluginOrgMount | null | undefined, hostIds: unknown): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
* How the organization's Inbox names a site (AGL-3303).
|
|
18
|
+
*
|
|
19
|
+
* A row holds a host DOCUMENT ID and a person reads a NAME; the shell's mount
|
|
20
|
+
* carries both for every site the reader may open. A site the mount cannot
|
|
21
|
+
* name is shown by its id rather than left blank — the row is real either way.
|
|
22
|
+
*/ export function orgSiteName(mount, hostId) {
|
|
23
|
+
var _mount_hosts_find;
|
|
24
|
+
if (typeof hostId !== 'string' || !hostId) return '';
|
|
25
|
+
return (mount == null ? void 0 : (_mount_hosts_find = mount.hosts.find((host)=>host.id === hostId)) == null ? void 0 : _mount_hosts_find.name) || hostId;
|
|
26
|
+
}
|
|
27
|
+
/** Every site a lead was captured by, by name, in capture order. */ export function orgSiteNames(mount, hostIds) {
|
|
28
|
+
return (Array.isArray(hostIds) ? hostIds : []).map((hostId)=>orgSiteName(mount, hostId)).filter(Boolean).join(', ');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=inbox-org-sites.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-org-sites.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsolePluginOrgMount } from '@aglyn/aglyn'\n\n/**\n * How the organization's Inbox names a site (AGL-3303).\n *\n * A row holds a host DOCUMENT ID and a person reads a NAME; the shell's mount\n * carries both for every site the reader may open. A site the mount cannot\n * name is shown by its id rather than left blank — the row is real either way.\n */\nexport function orgSiteName(\n mount: ConsolePluginOrgMount | null | undefined,\n hostId: unknown,\n): string {\n if (typeof hostId !== 'string' || !hostId) return ''\n return mount?.hosts.find((host) => host.id === hostId)?.name || hostId\n}\n\n/** Every site a lead was captured by, by name, in capture order. */\nexport function orgSiteNames(\n mount: ConsolePluginOrgMount | null | undefined,\n hostIds: unknown,\n): string {\n return (Array.isArray(hostIds) ? hostIds : [])\n .map((hostId) => orgSiteName(mount, hostId))\n .filter(Boolean)\n .join(', ')\n}\n"],"names":["orgSiteName","mount","hostId","hosts","find","host","id","name","orgSiteNames","hostIds","Array","isArray","map","filter","Boolean","join"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;CAMC,GACD,OAAO,SAASA,YACdC,KAA+C,EAC/CC,MAAe;QAGRD;IADP,IAAI,OAAOC,WAAW,YAAY,CAACA,QAAQ,OAAO;IAClD,OAAOD,CAAAA,0BAAAA,oBAAAA,MAAOE,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKC,EAAE,KAAKJ,4BAAxCD,kBAAiDM,IAAI,KAAIL;AAClE;AAEA,kEAAkE,GAClE,OAAO,SAASM,aACdP,KAA+C,EAC/CQ,OAAgB;IAEhB,OAAO,AAACC,CAAAA,MAAMC,OAAO,CAACF,WAAWA,UAAU,EAAE,AAAD,EACzCG,GAAG,CAAC,CAACV,SAAWF,YAAYC,OAAOC,SACnCW,MAAM,CAACC,SACPC,IAAI,CAAC;AACV"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ConsolePluginOrgMount } from '@aglyn/aglyn';
|
|
1
2
|
/**
|
|
2
3
|
* The Submissions section of the Inbox (AGL-77/104/109 → AGL-395): the form
|
|
3
4
|
* messages a site collected, newest first, and the reader that opens one.
|
|
@@ -8,7 +9,14 @@
|
|
|
8
9
|
* of them whichever one the URL names.
|
|
9
10
|
*/
|
|
10
11
|
export interface SubmissionsCardProps {
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The site whose submissions are listed, or `null` on the organization's
|
|
14
|
+
* Inbox, where the card lists every site's at once (AGL-3303) and each row
|
|
15
|
+
* is acted on as the site it was sent to.
|
|
16
|
+
*/
|
|
17
|
+
hostId: string | null;
|
|
18
|
+
/** The organization and its sites — present exactly when `hostId` is `null`. */
|
|
19
|
+
orgMount?: ConsolePluginOrgMount;
|
|
12
20
|
/**
|
|
13
21
|
* Narrow this card to ONE form, permanently.
|
|
14
22
|
*
|
|
@@ -28,7 +36,7 @@ export interface SubmissionsCardProps {
|
|
|
28
36
|
*/
|
|
29
37
|
formId?: string;
|
|
30
38
|
}
|
|
31
|
-
export declare function SubmissionsCard({ hostId, formId }: SubmissionsCardProps): import("react").JSX.Element;
|
|
39
|
+
export declare function SubmissionsCard({ hostId, formId, orgMount, }: SubmissionsCardProps): import("react").JSX.Element;
|
|
32
40
|
export declare namespace SubmissionsCard {
|
|
33
41
|
var displayName: string;
|
|
34
42
|
}
|