@aglyn/plugins-inbox 1.0.0-beta.222 → 1.0.0-beta.223
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/inbox-attribution-zone.d.ts +6 -0
- package/src/lib/components/inbox-attribution-zone.js.map +1 -1
- package/src/lib/components/submissions-card.component.js +52 -12
- package/src/lib/components/submissions-card.component.js.map +1 -1
- package/src/lib/model/submission-links.d.ts +70 -0
- package/src/lib/model/submission-links.js +74 -0
- package/src/lib/model/submission-links.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-inbox",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.223",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,15 +25,15 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
35
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
36
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.223",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.223",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.223",
|
|
31
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.223",
|
|
32
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.223",
|
|
33
|
+
"@aglyn/shared-util-email": "1.0.0-beta.223",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.223",
|
|
35
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.223",
|
|
36
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.223",
|
|
37
37
|
"@swc/helpers": "0.5.23"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -29,6 +29,12 @@ export interface InboxRecordAttributionZoneProps {
|
|
|
29
29
|
/** What the record is, in the Inbox's own words. */
|
|
30
30
|
recordKind: 'lead' | 'form';
|
|
31
31
|
recordId: string;
|
|
32
|
+
/**
|
|
33
|
+
* The containers the record is FILED under, by id (AGL-3461): for a
|
|
34
|
+
* submission, the campaigns its form and its page were filed under when it
|
|
35
|
+
* arrived. A widget draws them apart from what the record was credited to.
|
|
36
|
+
*/
|
|
37
|
+
filedUnder?: readonly string[];
|
|
32
38
|
}
|
|
33
39
|
export declare const INBOX_RECORD_ATTRIBUTION_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<InboxRecordAttributionZoneProps>;
|
|
34
40
|
export declare function InboxRecordAttributionZone(props: InboxRecordAttributionZoneProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-attribution-zone.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\n'use client'\n\nimport { listConsoleWidgets, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { Alert, AlertTitle } from '@mui/material'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * Where a lead or a submission came from, drawn by whichever plugin can say.\n *\n * The Inbox lists what arrived; which campaign or link brought it is a\n * question for a plugin that credits conversions. The Inbox used to import\n * the marketing plugin's component to answer it. It hosts this zone instead,\n * in a lead's \"Where this came from\" dialog and under an open submission, and\n * hands it the record's identity.\n */\nexport interface InboxRecordAttributionZoneProps {\n hostId: string\n /** What the record is, in the Inbox's own words. */\n recordKind: 'lead' | 'form'\n recordId: string\n}\n\nexport const INBOX_RECORD_ATTRIBUTION_ZONE =\n definePluginZone<InboxRecordAttributionZoneProps>('inboxRecordAttribution')\n\nexport function InboxRecordAttributionZone(\n props: InboxRecordAttributionZoneProps,\n) {\n const Slot = useConsoleWidgetSlot()\n if (!Slot) return null\n return <Slot slot={INBOX_RECORD_ATTRIBUTION_ZONE.id} {...props} />\n}\nInboxRecordAttributionZone.displayName = 'InboxRecordAttributionZone'\n\n/**\n * The Inbox's Campaigns section: a place, not a table.\n *\n * What arrived and who it came from sit beside what was sent to them, so the\n * Inbox keeps a Campaigns tab. The campaigns themselves belong to a plugin\n * that sends them, which draws its own card here; the Inbox hands it the site\n * and nothing else.\n *\n * On the organization's Inbox there is no site (AGL-3303): `hostId` is\n * `null` and `orgMount` names the organization and its sites, which is the\n * shell's own shape for a surface mounted with no site. A widget lists every\n * site's campaigns then, the way the organization's Marketing page does.\n */\nexport interface InboxCampaignsZoneProps {\n hostId: string | null\n /** Present exactly when `hostId` is `null`. */\n orgMount?: ConsolePluginOrgMount\n}\n\nexport const INBOX_CAMPAIGNS_ZONE =\n definePluginZone<InboxCampaignsZoneProps>('inboxCampaigns')\n\nexport function InboxCampaignsZone(props: InboxCampaignsZoneProps) {\n const Slot = useConsoleWidgetSlot()\n // Read at render, after the shell has loaded its plugins.\n if (!Slot || listConsoleWidgets(INBOX_CAMPAIGNS_ZONE.id).length === 0) {\n return (\n <Alert severity=\"info\">\n <AlertTitle>{'Campaigns are part of Marketing'}</AlertTitle>\n {props.hostId == null\n ? 'Marketing is switched off for this workspace, so there are no ' +\n 'campaigns to show here.'\n : 'Marketing is switched off for this workspace or this site, so ' +\n 'there are no campaigns to show here.'}\n </Alert>\n )\n }\n return <Slot slot={INBOX_CAMPAIGNS_ZONE.id} {...props} />\n}\nInboxCampaignsZone.displayName = 'InboxCampaignsZone'\n"],"names":["listConsoleWidgets","useConsoleWidgetSlot","Alert","AlertTitle","definePluginZone","INBOX_RECORD_ATTRIBUTION_ZONE","InboxRecordAttributionZone","props","Slot","slot","id","displayName","INBOX_CAMPAIGNS_ZONE","InboxCampaignsZone","length","severity","hostId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;AAEA,SAASA,kBAAkB,QAAoC,eAAc;AAC7E,SAASC,oBAAoB,QAAQ,qDAAoD;AACzF,SAASC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACjD,SAASC,gBAAgB,QAAQ,2CAA0C;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-attribution-zone.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\n'use client'\n\nimport { listConsoleWidgets, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { Alert, AlertTitle } from '@mui/material'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * Where a lead or a submission came from, drawn by whichever plugin can say.\n *\n * The Inbox lists what arrived; which campaign or link brought it is a\n * question for a plugin that credits conversions. The Inbox used to import\n * the marketing plugin's component to answer it. It hosts this zone instead,\n * in a lead's \"Where this came from\" dialog and under an open submission, and\n * hands it the record's identity.\n */\nexport interface InboxRecordAttributionZoneProps {\n hostId: string\n /** What the record is, in the Inbox's own words. */\n recordKind: 'lead' | 'form'\n recordId: string\n /**\n * The containers the record is FILED under, by id (AGL-3461): for a\n * submission, the campaigns its form and its page were filed under when it\n * arrived. A widget draws them apart from what the record was credited to.\n */\n filedUnder?: readonly string[]\n}\n\nexport const INBOX_RECORD_ATTRIBUTION_ZONE =\n definePluginZone<InboxRecordAttributionZoneProps>('inboxRecordAttribution')\n\nexport function InboxRecordAttributionZone(\n props: InboxRecordAttributionZoneProps,\n) {\n const Slot = useConsoleWidgetSlot()\n if (!Slot) return null\n return <Slot slot={INBOX_RECORD_ATTRIBUTION_ZONE.id} {...props} />\n}\nInboxRecordAttributionZone.displayName = 'InboxRecordAttributionZone'\n\n/**\n * The Inbox's Campaigns section: a place, not a table.\n *\n * What arrived and who it came from sit beside what was sent to them, so the\n * Inbox keeps a Campaigns tab. The campaigns themselves belong to a plugin\n * that sends them, which draws its own card here; the Inbox hands it the site\n * and nothing else.\n *\n * On the organization's Inbox there is no site (AGL-3303): `hostId` is\n * `null` and `orgMount` names the organization and its sites, which is the\n * shell's own shape for a surface mounted with no site. A widget lists every\n * site's campaigns then, the way the organization's Marketing page does.\n */\nexport interface InboxCampaignsZoneProps {\n hostId: string | null\n /** Present exactly when `hostId` is `null`. */\n orgMount?: ConsolePluginOrgMount\n}\n\nexport const INBOX_CAMPAIGNS_ZONE =\n definePluginZone<InboxCampaignsZoneProps>('inboxCampaigns')\n\nexport function InboxCampaignsZone(props: InboxCampaignsZoneProps) {\n const Slot = useConsoleWidgetSlot()\n // Read at render, after the shell has loaded its plugins.\n if (!Slot || listConsoleWidgets(INBOX_CAMPAIGNS_ZONE.id).length === 0) {\n return (\n <Alert severity=\"info\">\n <AlertTitle>{'Campaigns are part of Marketing'}</AlertTitle>\n {props.hostId == null\n ? 'Marketing is switched off for this workspace, so there are no ' +\n 'campaigns to show here.'\n : 'Marketing is switched off for this workspace or this site, so ' +\n 'there are no campaigns to show here.'}\n </Alert>\n )\n }\n return <Slot slot={INBOX_CAMPAIGNS_ZONE.id} {...props} />\n}\nInboxCampaignsZone.displayName = 'InboxCampaignsZone'\n"],"names":["listConsoleWidgets","useConsoleWidgetSlot","Alert","AlertTitle","definePluginZone","INBOX_RECORD_ATTRIBUTION_ZONE","InboxRecordAttributionZone","props","Slot","slot","id","displayName","INBOX_CAMPAIGNS_ZONE","InboxCampaignsZone","length","severity","hostId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;AAEA,SAASA,kBAAkB,QAAoC,eAAc;AAC7E,SAASC,oBAAoB,QAAQ,qDAAoD;AACzF,SAASC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACjD,SAASC,gBAAgB,QAAQ,2CAA0C;AAwB3E,OAAO,MAAMC,gCACXD,iBAAkD,0BAAyB;AAE7E,OAAO,SAASE,2BACdC,KAAsC;IAEtC,MAAMC,OAAOP;IACb,IAAI,CAACO,MAAM,OAAO;IAClB,qBAAO,KAACA;QAAKC,MAAMJ,8BAA8BK,EAAE;OAAMH;AAC3D;AACAD,2BAA2BK,WAAW,GAAG;AAqBzC,OAAO,MAAMC,uBACXR,iBAA0C,kBAAiB;AAE7D,OAAO,SAASS,mBAAmBN,KAA8B;IAC/D,MAAMC,OAAOP;IACb,0DAA0D;IAC1D,IAAI,CAACO,QAAQR,mBAAmBY,qBAAqBF,EAAE,EAAEI,MAAM,KAAK,GAAG;QACrE,qBACE,MAACZ;YAAMa,UAAS;;8BACd,KAACZ;8BAAY;;gBACZI,MAAMS,MAAM,IAAI,OACb,mEACA,4BACA,mEACA;;;IAGV;IACA,qBAAO,KAACR;QAAKC,MAAMG,qBAAqBF,EAAE;OAAMH;AAClD;AACAM,mBAAmBF,WAAW,GAAG"}
|
|
@@ -18,6 +18,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
19
|
import { FORMS_MAX_PER_HOST, pluginDocsHelp } from "@aglyn/aglyn";
|
|
20
20
|
import { INBOX_SUBMISSION_PARAM } from "../model/inbox-record-routes.js";
|
|
21
|
+
import { hostPublicOrigin } from "@aglyn/aglyn/app-utils/host-naming";
|
|
22
|
+
import { submissionFiledUnder, submissionLinks } from "../model/submission-links.js";
|
|
21
23
|
/** The forms plugin's record kind this card reads (AGL-3080). */ const FORM_SUBMISSION_KIND = 'formSubmission';
|
|
22
24
|
// A deep import, NOT the plugin barrel (AGL-1151): the barrel is the entry
|
|
23
25
|
// point the tenant's loader dynamically imports to activate the marketing
|
|
@@ -26,9 +28,9 @@ import { INBOX_SUBMISSION_PARAM } from "../model/inbox-record-routes.js";
|
|
|
26
28
|
import { InboxRecordAttributionZone } from "./inbox-attribution-zone.js";
|
|
27
29
|
// The CRM's route builder by its leaf path, for the reason above: the barrel
|
|
28
30
|
// is the plugin's site entry point.
|
|
29
|
-
import { pluginRecordByEmailHref, pluginRecordListHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
31
|
+
import { pluginRecordByEmailHref, pluginRecordHref, pluginRecordListHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
30
32
|
import { mdiAccountArrowRight, mdiDeleteOutline, mdiEmailOpenOutline, mdiEmailOutline } from "@aglyn/shared-data-mdi";
|
|
31
|
-
import { CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
33
|
+
import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
32
34
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
33
35
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
34
36
|
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
@@ -71,8 +73,8 @@ const SUBMISSION_HIDDEN_COLUMNS = {
|
|
|
71
73
|
return Object.entries((_ref = submission == null ? void 0 : submission.fields) != null ? _ref : {}).map(([key, value])=>`${key}: ${value}`).join(' · ');
|
|
72
74
|
};
|
|
73
75
|
export function SubmissionsCard({ hostId, formId, orgMount }) {
|
|
74
|
-
var _ref, _ref1, _ref2, _ref3, _ref4, _ref5;
|
|
75
|
-
var _reader_createdAt_toDate, _reader_createdAt;
|
|
76
|
+
var _ref, _ref1, _ref2, _ref3, _ref4, _hostPublicOrigin, _ref5, _ref6, _ref7;
|
|
77
|
+
var _reader_createdAt_toDate, _reader_createdAt, _orgMount_hosts_find;
|
|
76
78
|
const firestore = useFirestore();
|
|
77
79
|
const { enqueueSnackbar } = useSnackbar();
|
|
78
80
|
const postPluginApi = usePluginApiPost();
|
|
@@ -659,15 +661,52 @@ export function SubmissionsCard({ hostId, formId, orgMount }) {
|
|
|
659
661
|
}),
|
|
660
662
|
/*#__PURE__*/ _jsxs(DialogContent, {
|
|
661
663
|
children: [
|
|
662
|
-
/*#__PURE__*/
|
|
664
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
663
665
|
variant: "caption",
|
|
664
666
|
color: "text.secondary",
|
|
665
|
-
children:
|
|
666
|
-
|
|
667
|
-
(hostId == null && readerSite ? ` · ${orgSiteName(orgMount, readerSite)}` : '') + ((reader == null ? void 0 : reader.screenId) ? ` · page ${reader.screenId}` : ''),
|
|
668
|
-
(reader == null ? void 0 : reader.path) ? ` · ${String(reader.path)}` : ''
|
|
669
|
-
]
|
|
667
|
+
children: `Received ${(_ref4 = reader == null ? void 0 : (_reader_createdAt = reader.createdAt) == null ? void 0 : (_reader_createdAt_toDate = _reader_createdAt.toDate) == null ? void 0 : _reader_createdAt_toDate.call(_reader_createdAt).toLocaleString()) != null ? _ref4 : ''}` + // The site it was sent to, where the list spans every site.
|
|
668
|
+
(hostId == null && readerSite ? ` · ${orgSiteName(orgMount, readerSite)}` : '')
|
|
670
669
|
}),
|
|
670
|
+
reader ? /*#__PURE__*/ _jsx(Stack, {
|
|
671
|
+
spacing: 0.5,
|
|
672
|
+
sx: {
|
|
673
|
+
mt: 1
|
|
674
|
+
},
|
|
675
|
+
children: submissionLinks({
|
|
676
|
+
submission: reader,
|
|
677
|
+
hrefOf: (kind, id)=>routeContext ? pluginRecordHref(kind, routeContext, id) : null,
|
|
678
|
+
siteOrigin: (_hostPublicOrigin = hostPublicOrigin({
|
|
679
|
+
subdomain: (_ref5 = (_ref6 = hostId != null ? routeContext == null ? void 0 : routeContext.host : null) != null ? _ref6 : orgMount == null ? void 0 : (_orgMount_hosts_find = orgMount.hosts.find((site)=>site.id === readerSite)) == null ? void 0 : _orgMount_hosts_find.subdomain) != null ? _ref5 : null
|
|
680
|
+
})) != null ? _hostPublicOrigin : null
|
|
681
|
+
}).map((link)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
682
|
+
direction: "row",
|
|
683
|
+
spacing: 1,
|
|
684
|
+
sx: {
|
|
685
|
+
alignItems: 'baseline'
|
|
686
|
+
},
|
|
687
|
+
children: [
|
|
688
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
689
|
+
variant: "caption",
|
|
690
|
+
color: "text.secondary",
|
|
691
|
+
sx: {
|
|
692
|
+
minWidth: 72
|
|
693
|
+
},
|
|
694
|
+
children: link.label
|
|
695
|
+
}),
|
|
696
|
+
link.href ? /*#__PURE__*/ _jsx(AppLink, _extends({
|
|
697
|
+
href: link.href
|
|
698
|
+
}, link.external ? {
|
|
699
|
+
target: '_blank',
|
|
700
|
+
rel: 'noopener'
|
|
701
|
+
} : {}, {
|
|
702
|
+
children: link.text
|
|
703
|
+
})) : /*#__PURE__*/ _jsx(Typography, {
|
|
704
|
+
variant: "body2",
|
|
705
|
+
children: link.text
|
|
706
|
+
})
|
|
707
|
+
]
|
|
708
|
+
}, link.key))
|
|
709
|
+
}) : null,
|
|
671
710
|
/*#__PURE__*/ _jsx(Divider, {
|
|
672
711
|
sx: {
|
|
673
712
|
my: 1.5
|
|
@@ -675,7 +714,7 @@ export function SubmissionsCard({ hostId, formId, orgMount }) {
|
|
|
675
714
|
}),
|
|
676
715
|
/*#__PURE__*/ _jsx(Stack, {
|
|
677
716
|
spacing: 1.5,
|
|
678
|
-
children: Object.entries((
|
|
717
|
+
children: Object.entries((_ref7 = reader == null ? void 0 : reader.fields) != null ? _ref7 : {}).map(([key, value])=>/*#__PURE__*/ _jsxs(Stack, {
|
|
679
718
|
spacing: 0.25,
|
|
680
719
|
children: [
|
|
681
720
|
/*#__PURE__*/ _jsx(Typography, {
|
|
@@ -716,7 +755,8 @@ export function SubmissionsCard({ hostId, formId, orgMount }) {
|
|
|
716
755
|
children: /*#__PURE__*/ _jsx(InboxRecordAttributionZone, {
|
|
717
756
|
hostId: readerSite,
|
|
718
757
|
recordKind: "form",
|
|
719
|
-
recordId: String(reader.$id)
|
|
758
|
+
recordId: String(reader.$id),
|
|
759
|
+
filedUnder: submissionFiledUnder(reader)
|
|
720
760
|
})
|
|
721
761
|
}) : null,
|
|
722
762
|
reader && readerSite ? /*#__PURE__*/ _jsx(SubmissionReply, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/submissions-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 FORMS_MAX_PER_HOST,\n pluginDocsHelp,\n type ConsolePluginOrgMount,\n} from '@aglyn/aglyn'\nimport { INBOX_SUBMISSION_PARAM } from '../model/inbox-record-routes'\n\n/** The forms plugin's record kind this card reads (AGL-3080). */\nconst FORM_SUBMISSION_KIND = 'formSubmission'\n// A deep import, NOT the plugin barrel (AGL-1151): the barrel is the entry\n// point the tenant's loader dynamically imports to activate the marketing\n// plugin's SITE half, so a console card named there ships to every published\n// page. The component path reaches the same module without crossing it.\nimport { InboxRecordAttributionZone } from './inbox-attribution-zone'\n// The CRM's route builder by its leaf path, for the reason above: the barrel\n// is the plugin's site entry point.\nimport {\n pluginRecordByEmailHref,\n pluginRecordListHref,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport {\n mdiAccountArrowRight,\n mdiDeleteOutline,\n mdiEmailOpenOutline,\n mdiEmailOutline,\n} from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport {\n type ListFilterOption,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n usePluginApiPost,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport {\n Avatar,\n Box,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Divider,\n Stack,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n deleteDoc,\n getDoc,\n limit,\n orderBy,\n query,\n updateDoc,\n} from 'firebase/firestore'\n// The submissions are the forms plugin's records, walked and opened through\n// the list source it publishes (AGL-3080).\nimport {\n pluginRecordListDoc,\n pluginRecordListWalk,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport { useSearchParams } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n relativeTime,\n routingChips,\n senderHue,\n submissionSender,\n} from '../model/submission-presenter'\nimport {\n FORM_SCOPED_SUBMISSION_LIST_QUERY,\n ORG_SUBMISSION_LIST_QUERY,\n SUBMISSION_FILTER_HEADERS,\n SUBMISSION_LIST_QUERY,\n SUBMISSION_READ_OPTIONS,\n formSubmissionBase,\n orgSubmissionBase,\n} from '../constants/list-queries'\nimport { orgSiteName } from './inbox-org-sites'\nimport SubmissionListAssignment from './submission-list-assignment.component'\nimport SubmissionReply from './submission-reply.component'\nimport { useRecordRouteContext } from './use-record-route-context'\n\n\n/**\n * The forms plugin's recount route (AGL-3330), by address: this plugin\n * shows the submissions a form counts and may not import the plugin that\n * keeps the counts.\n */\nconst FORM_STATS_API_PATH = '/api/forms/stats'\n/** Read, Form and Site are picked, so the panel shows selects. */\nconst SUBMISSION_SELECT_FIELDS = ['read', 'formId', 'hostId']\nconst SUBMISSION_HIDDEN_COLUMNS = { read: false, formId: false }\n\n/** Every field of a message, as one line — what the Message column draws. */\nconst messageTextOf = (submission: any): string =>\n Object.entries(submission?.fields ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join(' · ')\n\n/**\n * The Submissions section of the Inbox (AGL-77/104/109 → AGL-395): the form\n * messages a site collected, newest first, and the reader that opens one.\n *\n * Its own component since AGL-2501, when the Inbox's tabs became routes. The\n * split is what makes \"mount only the section being read\" structural: hooks\n * cannot be conditional, so a page holding every section's reads pays for all\n * of them whichever one the URL names.\n */\nexport interface SubmissionsCardProps {\n /**\n * The site whose submissions are listed, or `null` on the organization's\n * Inbox, where the card lists every site's at once (AGL-3303) and each row\n * is acted on as the site it was sent to.\n */\n hostId: string | null\n /** The organization and its sites — present exactly when `hostId` is `null`. */\n orgMount?: ConsolePluginOrgMount\n /**\n * Narrow this card to ONE form, permanently.\n *\n * What the forms plugin's detail surface renders instead of a second\n * submissions table. The reader has already chosen the subject by being on\n * that page, so scoping here does three things a copy would have had to\n * re-derive: the form picker is not rendered (there is nothing to pick),\n * the site's `forms` collection is not read at all (the picker was its only\n * consumer), and the empty state names the form rather than the site.\n *\n * Everything else — the paged walk, the reader dialog, read/unread,\n * delete, reply, list assignment, attribution — is the same code answering\n * a narrower query. That is the reason this is a prop rather than a second\n * component: a per-form table written separately would be a second reader\n * to keep in step, and it would have been the one to reintroduce the\n * unordered `limit()` this card's own comment exists to warn about.\n */\n formId?: string\n}\n\nexport function SubmissionsCard({\n hostId,\n formId,\n orgMount,\n}: SubmissionsCardProps) {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const postPluginApi = usePluginApiPost()\n const { confirm } = useConfirmationContext()\n /** Scoped to one form: the subject is fixed and nothing may widen it. */\n const scoped = Boolean(formId)\n /** Every site of this organization at once, when no site is named. */\n const orgId = hostId == null ? (orgMount?.orgId ?? null) : null\n /*\n * The site a row lives on, which is where every act on it is addressed:\n * this card's own site, or — across every site — the `hostId` the submit\n * route stamps on the row. That stamp is frozen against a client update and\n * written from the row's own path, so it names the document's real parent;\n * the rules check the host role on that site either way.\n */\n const siteOf = useCallback(\n (submission: any): string | null =>\n hostId ??\n (typeof submission?.hostId === 'string' && submission.hostId\n ? submission.hostId\n : null),\n [hostId],\n )\n /*\n * Where the sender's CONTACT is (AGL-2612). A submission that carried an\n * address updated a contact in the CRM at stage Lead; the row links to\n * it by that address, and the Contacts list — which holds the id nothing\n * here does — opens the record. The same hub path the Members & leads\n * rows use to open a lead.\n */\n const routeContext = useRecordRouteContext()\n\n /*\n * The site's forms, for the Submissions filter.\n *\n * `FORMS_MAX_PER_HOST` is a read WINDOW, not a cap on the collection — how\n * many forms a site may hold is `formsPerHost`, enforced at the create in\n * `/api/hosts/resources`, and a staff-set per-org override can raise it\n * past this window. So the window can be smaller than the catalog, and one\n * more document than fits is read on purpose: a filter that quietly listed\n * the first N would report \"this form does not exist\" and \"this form is\n * past the window\" as the same empty answer.\n *\n * Ordered by `__name__`. `displayName` would be the nicer order and is the\n * wrong instrument: `orderBy` on a data field DROPS every document missing\n * it, invisibly, so a form saved without a name would vanish from its own\n * filter. The list is sorted for display below, where a missing name is\n * merely ugly.\n */\n const { data: formDocs } = useFirestoreCollection<any>(\n () =>\n // Forms are one site's catalog. Across every site there is no single\n // catalog to filter by, and picking a site is how a reader gets one.\n scoped || !hostId\n ? null\n : query(\n collection(firestore, 'hosts', hostId, 'forms'),\n orderBy('__name__'),\n limit(FORMS_MAX_PER_HOST + 1),\n ),\n [firestore, hostId, scoped],\n { idField: '$id' },\n )\n /** More forms exist than the window shows; the filter has to say so. */\n const formsTruncated = (formDocs?.length ?? 0) > FORMS_MAX_PER_HOST\n const forms = useMemo(\n () =>\n [...(formDocs ?? [])]\n .slice(0, FORMS_MAX_PER_HOST)\n .sort((left: any, right: any) =>\n String(left.displayName ?? left.$id).localeCompare(\n String(right.displayName ?? right.$id),\n ),\n ),\n [formDocs],\n )\n /** Form is offered where the site's catalog is read. */\n const offersForm = !scoped && Boolean(hostId) && forms.length > 0\n /*\n * WHAT THIS CARD'S LIST CAN BE ASKED (AGL-3321) — every clause and the\n * search on the query, see `SUBMISSION_LIST_QUERY`:\n *\n * - a site's Inbox: From, Read and — where the site has forms — Form;\n * - a card scoped to one form: that form is the list's own scope, and\n * Form has nothing left to pick, so it is not offered. The scope wins\n * over any clause: it is the base, and no clause can reach it;\n * - every site's, on the organization's Inbox: From, Read and Site,\n * under the `orgId` clause the rules require of the group read.\n */\n const declaration = useMemo<ListQueryDeclaration>(() => {\n if (scoped) return FORM_SCOPED_SUBMISSION_LIST_QUERY\n if (hostId == null) return ORG_SUBMISSION_LIST_QUERY\n return offersForm\n ? SUBMISSION_LIST_QUERY\n : {\n ...SUBMISSION_LIST_QUERY,\n fields: SUBMISSION_LIST_QUERY.fields.filter((field) => field.column !== 'formId'),\n }\n }, [scoped, hostId, offersForm])\n const base = useMemo(\n () =>\n formId ? formSubmissionBase(formId) : hostId == null && orgId ? orgSubmissionBase(orgId) : [],\n [formId, hostId, orgId],\n )\n const gridFilter = useListGridFilter({ selectFields: SUBMISSION_SELECT_FIELDS })\n\n /*\n * The inbox WALKS its submissions instead of sampling them (AGL-2501,\n * AGL-2292), and every filter and search word is a predicate on the walk\n * (AGL-3321), so a page is a page of the matches.\n *\n * `limit(200)` carried no `orderBy`, so Firestore answered it in\n * DOCUMENT-ID order over ids `add()` generates — an arbitrary two hundred\n * of the site's messages, which the client sort then arranged newest-first\n * so the result looked like a feed. A site past two hundred submissions\n * could not reach the rest, and the messages missing left no gap: the row\n * dates on screen simply skipped, which reads as a quiet week rather than\n * as an unreachable inbox.\n *\n * `createdAt` is safe to order on, checked against the writer rather than\n * assumed: `libs/plugins/forms/src/lib/server/form-submit.ts` is the only path\n * that creates one and stamps `createdAt: serverTimestamp()` on every add,\n * the v1 API only ever reads and deletes, and `formSubmissions` is absent\n * from `IMPORTABLE_FIELDS`, so no restore path can make one without it.\n *\n * EVERY SITE'S SUBMISSIONS IN ONE QUERY on the organization's Inbox\n * (AGL-3303), not a listener per site: the collection group, newest first.\n * The `orgId` clause is not a filter this card could drop. The rules admit\n * a group read only when it is narrowed to an org the reader spans, so an\n * unfiltered group query is refused outright. Every submission carries\n * `orgId`: the submit route stamps it, and the rows written before it did\n * were stamped once (AGL-3303). A row without one would be in no\n * organization's list, though its own site's Inbox lists it either way.\n */\n const {\n rows: submissions,\n hasMore: hasMoreSubmissions,\n page: submissionPage,\n setPage: setSubmissionPage,\n pageSize: submissionPageSize,\n setPageSize: setSubmissionPageSize,\n plan,\n } = useListQuery<any>({\n collection: pluginRecordListWalk(FORM_SUBMISSION_KIND, firestore, { hostId, orgId }),\n declaration,\n request: { clauses: gridFilter.clauses, search: gridFilter.searchWords, base },\n deps: [firestore, hostId, orgId],\n idField: '$id',\n })\n const submissionOptions = useMemo<Readonly<Record<string, readonly ListFilterOption[]>>>(\n () => ({\n read: SUBMISSION_READ_OPTIONS,\n ...(offersForm\n ? {\n formId: forms.map((form: any) => ({\n value: String(form.$id),\n label: String(form.displayName || form.$id),\n })),\n }\n : {}),\n ...(hostId == null\n ? {\n hostId: (orgMount?.hosts ?? []).map((host) => ({\n value: host.id,\n label: host.name || host.id,\n })),\n }\n : {}),\n }),\n [offersForm, forms, hostId, orgMount],\n )\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: declaration.fields,\n headers: SUBMISSION_FILTER_HEADERS,\n options: submissionOptions,\n }),\n [plan.refused, declaration, submissionOptions],\n )\n const searching = gridFilter.searchWords.some((word) => word.trim())\n const filtering = gridFilter.clauses.length > 0 || searching\n /** Narrowed by nothing but a Form clause. */\n const onlyForm =\n !searching &&\n gridFilter.clauses.length > 0 &&\n gridFilter.clauses.every((clause) => clause.field === 'formId')\n\n // Mail reader (AGL-104): opening a submission shows the full message and\n // marks it read.\n const [reader, setReader] = useState<any | null>(null)\n const handleOpenReader = useCallback(\n (submission: any) => () => {\n setReader(submission)\n const site = siteOf(submission)\n const submissionDoc = site\n ? pluginRecordListDoc(FORM_SUBMISSION_KIND, firestore, { hostId: site, id: submission.$id })\n : null\n if (!submission.read && submissionDoc) {\n void updateDoc(submissionDoc, { read: true })\n }\n },\n [firestore, siteOf],\n )\n\n const handleToggleRead = useCallback(\n (submission: any) => () => {\n const site = siteOf(submission)\n const submissionDoc = site\n ? pluginRecordListDoc(FORM_SUBMISSION_KIND, firestore, { hostId: site, id: submission.$id })\n : null\n if (!submissionDoc) return\n void updateDoc(submissionDoc, { read: !submission.read })\n },\n [firestore, siteOf],\n )\n\n /*\n * A submission named in the URL opens in the reader on arrival (AGL-2622).\n *\n * A contact's timeline names the submission that captured the person, and\n * the address it links to is this tab with `?submission={id}`. The\n * document is read once, by id, rather than looked for in the paged\n * window: the window is the newest page of a walk that may not reach a\n * submission from months back, and one document read is what the URL\n * asked for. Opening it marks it read, as a click on the row would. A\n * submission that is gone says so, because a reader that silently stays\n * shut reads as the link having done nothing.\n *\n * Once per id: the ref keeps a re-render — and the same URL re-settling —\n * from reopening a reader the person has since closed. The landing read\n * is judged against that ref rather than an effect cleanup: a listener\n * settling while the read is in flight re-renders this card, and a\n * cleanup keyed on any dep would cancel the answer to the question the\n * URL just asked.\n */\n const searchParams = useSearchParams()\n const seededSubmissionId = searchParams?.get(INBOX_SUBMISSION_PARAM) ?? null\n const seededOpened = useRef<string | null>(null)\n useEffect(() => {\n // A submission link names one site's Inbox; the org's has no site to read\n // the id under.\n if (!hostId) return\n if (!seededSubmissionId || seededOpened.current === seededSubmissionId) return\n seededOpened.current = seededSubmissionId\n const seededDoc = pluginRecordListDoc(FORM_SUBMISSION_KIND, firestore, {\n hostId,\n id: seededSubmissionId,\n })\n if (!seededDoc) return\n void getDoc(seededDoc)\n .then((snapshot) => {\n if (seededOpened.current !== snapshot.id) return\n if (!snapshot.exists()) {\n enqueueSnackbar('That submission is no longer in the Inbox.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n handleOpenReader({ ...snapshot.data(), $id: snapshot.id })()\n })\n .catch(() => undefined)\n }, [seededSubmissionId, firestore, hostId, handleOpenReader, enqueueSnackbar])\n\n const handleDelete = useCallback(\n (submission: any) => async () => {\n const site = siteOf(submission)\n const submissionDoc = site\n ? pluginRecordListDoc(FORM_SUBMISSION_KIND, firestore, { hostId: site, id: submission.$id })\n : null\n if (!site || !submissionDoc) return\n const confirmed = await confirm({\n title: 'Delete this submission?',\n description: 'The submission is removed permanently.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await deleteDoc(submissionDoc)\n enqueueSnackbar('Submission deleted', {\n variant: 'success',\n persist: false,\n })\n /*\n * The form counted this submission when it arrived (AGL-3330), and an\n * increment cannot see a delete: the forms plugin recounts the form\n * from the rows that are left. Best effort — the delete has happened\n * either way, and the recount script catches a follow-up that failed.\n */\n if (typeof submission.formId === 'string' && submission.formId) {\n void postPluginApi(FORM_STATS_API_PATH, { hostId: site, formIds: [submission.formId] })\n }\n },\n [confirm, firestore, siteOf, enqueueSnackbar, postPluginApi],\n )\n\n /*\n * One row per submission, newest first: who sent it, what it said and how\n * long ago. The row opens the reader, which also marks it read.\n */\n const submissionActions = (submission: any): RowActionsMenuItem[] => {\n const senderEmail = submissionSender(submission.fields).email\n // Where the sender's contact is read, asked of whichever plugin keeps\n // people; with none loaded there is no such page, and no menu item.\n const contactsHref = routeContext\n ? pluginRecordListHref('contact', routeContext)\n : null\n return [\n ...(routeContext && contactsHref\n ? [\n {\n key: 'crm',\n label: 'Open contact in CRM',\n icon: <MdiIcon path={mdiAccountArrowRight.path} size={0.8} />,\n ...(senderEmail\n ? {\n href:\n pluginRecordByEmailHref(\n 'contact',\n routeContext,\n senderEmail,\n ) ?? contactsHref,\n }\n : {\n disabled: true,\n disabledReason:\n 'This submission carried no email address, so no contact was updated.',\n }),\n },\n ]\n : []),\n {\n key: 'read',\n label: submission.read ? 'Mark unread' : 'Mark read',\n icon: (\n <MdiIcon\n path={submission.read ? mdiEmailOutline.path : mdiEmailOpenOutline.path}\n size={0.8}\n />\n ),\n onClick: handleToggleRead(submission),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: handleDelete(submission),\n },\n ]\n }\n const submissionColumns: GridColDef[] = [\n {\n /*\n The mockup's list is people, not forms (AGL-2168): an initials avatar,\n the sender, and the form name beneath it. The unread DOT replaces the\n `New` chip — the row is already bold, and a chip that says \"New\" beside\n bold text is the same fact twice.\n */\n field: 'from',\n headerName: 'From',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, submission) => submissionSender(submission.fields).label,\n renderCell: ({ row: submission }) => {\n const sender = submissionSender(submission.fields)\n const hue = senderHue(sender.label)\n return (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', minWidth: 0 }}>\n {!submission.read ? (\n <Box\n aria-label=\"Unread\"\n sx={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n bgcolor: 'primary.main',\n flexShrink: 0,\n }}\n />\n ) : (\n <Box sx={{ width: 8, flexShrink: 0 }} />\n )}\n <Avatar\n sx={{\n width: 28,\n height: 28,\n fontSize: 13,\n bgcolor: `hsl(${hue} 55% 45%)`,\n }}\n >\n {sender.initials}\n </Avatar>\n <Stack sx={{ 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 </Stack>\n )\n },\n },\n /*\n Which site it was sent to, on the organization's Inbox only — under a\n site every row is that site's, and a column saying so would be noise.\n */\n ...(hostId == null\n ? [\n {\n field: 'hostId',\n headerName: 'Site',\n flex: 1,\n minWidth: 140,\n // Sorted and drawn by name; the filter matches the id.\n valueGetter: (_value: unknown, submission: any) =>\n orgSiteName(orgMount, submission.hostId),\n renderCell: ({ row: submission }: { row: any }) =>\n orgSiteName(orgMount, submission.hostId),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'message',\n headerName: 'Message',\n flex: 2,\n minWidth: 260,\n valueGetter: (_value, submission) => messageTextOf(submission),\n renderCell: ({ value }) => (\n <Typography variant=\"body2\" noWrap>\n {value}\n </Typography>\n ),\n },\n {\n /*\n Relative, as the mockup shows it — an inbox is scanned for recency and a\n locale timestamp makes the reader do the subtraction. The absolute time\n stays on the detail dialog, where it is the fact you actually want.\n */\n field: 'createdAt',\n headerName: 'Received',\n width: 130,\n valueGetter: (_value, submission) =>\n submission.createdAt?.toDate?.()?.getTime?.() ?? 0,\n renderCell: ({ row: submission }) => (\n <Tooltip title={submission.createdAt?.toDate?.().toLocaleString() ?? ''}>\n <span>{relativeTime(submission.createdAt?.toDate?.().getTime())}</span>\n </Tooltip>\n ),\n },\n /*\n READ, as the panel picks it — never drawn: the unread dot and the bold\n row already say it. A boolean field has no hidden column of its own (the\n grid's boolean operators are not the select's), so it is declared here,\n kept hidden and out of Manage columns.\n */\n {\n field: 'read',\n headerName: 'Read',\n hideable: false,\n valueGetter: (_value, submission) => String(Boolean(submission.read)),\n },\n /*\n One overflow menu, as the Members & leads rows have: two inline buttons\n had no room for a third, and the contact link is the one a reader\n reaches for after reading. Present but disabled for a submission with no\n address, with the reason — a row that simply lacked the item would read\n as the contact not existing.\n */\n listActionsColumn(\n (submission) => (\n <ListRowActions\n label={String(submissionSender(submission.fields).email ?? submission.$id)}\n items={submissionActions(submission)}\n />\n ),\n { width: 72 },\n ),\n ]\n\n /** The site the open submission was sent to, which its reader acts as. */\n const readerSite = reader ? siteOf(reader) : null\n\n return (\n <>\n <CardDisplay\n header={scoped ? 'Submissions to this form' : 'Form Submissions'}\n help={pluginDocsHelp(\n 'forms',\n hostId == null\n ? {\n anchor: '#every-sites-inbox-at-once',\n excerpt:\n 'Every site’s form messages in one list, newest first, ' +\n 'each under the site it was sent to.',\n }\n : {\n anchor: '#the-inbox',\n excerpt:\n 'Messages your forms collected, newest first, showing who ' +\n 'sent each one and where it was routed.',\n },\n )}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n {/*\n * The Inbox stays the site-wide answer to \"who is waiting for a\n * reply\" — that question does not decompose by form. The panel's\n * Form filter narrows it on request; it does not turn the page into\n * a per-form view. It is offered only when the site HAS forms, and\n * never on a card already scoped to one.\n */}\n {offersForm && formsTruncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\" sx={{ mb: 1 }}>\n {`The Form filter offers the first ${FORMS_MAX_PER_HOST.toLocaleString()} ` +\n 'forms. Narrow by form is incomplete; with no Form filter the ' +\n 'list still covers every submission.'}\n </Typography>\n ) : null}\n {hostId == null ? (\n <Typography\n variant=\"body2\"\n color=\"text.secondary\"\n sx={{ mb: submissions.length ? 2 : 1 }}\n >\n {'Every site’s submissions, newest first. Choose a site to ' +\n 'narrow them by form.'}\n </Typography>\n ) : null}\n {submissions.length === 0 && !hasMoreSubmissions && !filtering ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {scoped\n ? 'No submissions carry this form’s id yet. Messages this ' +\n 'form’s design collected before it became a form entity are ' +\n 'in the Inbox, filed under the name they were sent with.'\n : hostId == null\n ? 'No form submissions on any site yet.'\n : 'No form submissions yet. Add a Contact Form element to a ' +\n 'page — visitor messages arrive here.'}\n </Typography>\n ) : (\n <>\n <ListFilterChips\n fields={declaration.fields}\n headers={SUBMISSION_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={submissionOptions}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label={scoped ? 'Submissions to this form' : 'Form submissions'}\n rows={submissions}\n columns={listFilterGridColumns(\n submissionColumns,\n declaration.fields,\n submissionOptions,\n SUBMISSION_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: SUBMISSION_HIDDEN_COLUMNS } }}\n noRowsLabel={\n onlyForm\n ? 'No submissions for this form yet. Submissions sent before ' +\n 'the form was created are listed with no Form filter.'\n : 'No submissions match these filters'\n }\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, submission) => handleOpenReader(submission)()}\n // An unread submission reads bold across its row, which is why\n // the list needs no `New` chip.\n getRowClassName={({ row }) => (row.read ? '' : 'submission-unread')}\n sx={{\n '& .submission-unread .MuiDataGrid-cell, & .submission-unread .MuiTypography-root':\n { fontWeight: 'fontWeightBold' },\n }}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to the query; the grid neither\n // filters nor sorts the page it holds.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n />\n <ListPagination\n page={submissionPage}\n pageSize={submissionPageSize}\n rowCount={submissions.length}\n hasMore={hasMoreSubmissions}\n onPageChange={setSubmissionPage}\n onPageSizeChange={setSubmissionPageSize}\n />\n </>\n )}\n </CardDisplay>\n <Dialog\n open={Boolean(reader)}\n onClose={() => setReader(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{reader?.formName ?? 'Form submission'}</DialogTitle>\n <DialogContent>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Received ${reader?.createdAt?.toDate?.().toLocaleString() ?? ''}` +\n // The site it was sent to, where the list spans every site.\n (hostId == null && readerSite\n ? ` · ${orgSiteName(orgMount, readerSite)}`\n : '') +\n (reader?.screenId ? ` · page ${reader.screenId}` : '')}\n {/*\n THE PAGE THE FORM WAS ON. Stored by the submit route since the\n form existed and rendered by nothing, which is also the field\n the marketing console's landing-page grouping joins on — a\n reader who wants to check one row against that grouping has to\n be able to see the row's own page.\n */}\n {reader?.path ? ` · ${String(reader.path)}` : ''}\n </Typography>\n <Divider sx={{ my: 1.5 }} />\n <Stack spacing={1.5}>\n {Object.entries(reader?.fields ?? {}).map(([key, value]) => (\n <Stack key={key} spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {key}\n </Typography>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n >\n {String(value)}\n </Typography>\n </Stack>\n ))}\n </Stack>\n {/*\n What happened to this submission (AGL-2168). The mockup puts\n these under the fields: `Saved to Inbox` and `Added to \"Leads\"\n dataset`. The second is stamped by the submit route only when a\n record was really appended — a form bound to a deleted dataset,\n or one whose record quota is full, shows no chip rather than a\n chip for a row that does not exist. Both are failures the route\n already swallows silently, and a chip that lied about them\n would be worse than the silence.\n */}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ mt: 2, flexWrap: 'wrap', rowGap: 1 }}\n >\n {routingChips(reader?.routing).map((chip) => (\n <Chip\n key={chip.label}\n size=\"small\"\n label={chip.label}\n color={chip.color}\n variant=\"outlined\"\n />\n ))}\n </Stack>\n {/*\n WHERE THIS SUBMISSION CAME FROM.\n\n One keyed document read — the attribution's id is `form:{id}` and\n the submission's id is that `{id}` — paid when a merchant opens a\n submission rather than once per row of the list. A submission that\n was credited to nobody renders the sentence saying so; it never\n renders a campaign with a zero beside it.\n */}\n {reader?.$id && readerSite ? (\n <Box sx={{ mt: 2 }}>\n <InboxRecordAttributionZone\n hostId={readerSite}\n recordKind=\"form\"\n recordId={String(reader.$id)}\n />\n </Box>\n ) : null}\n {/*\n Answering the person is the act the Inbox exists for, and it is\n inside the reader rather than on the row because a reply written\n without the message in front of you is the reply that answers the\n wrong question. Mounted with the dialog, so its reads — the site\n name and the replies already sent — are paid when a merchant opens\n a submission and not once per visit to this page.\n */}\n {reader && readerSite ? (\n <SubmissionReply hostId={readerSite} submission={reader} />\n ) : null}\n {/*\n Enrolling the sender in a marketing list — a SEPARATE act from\n answering them, and a separate card, because the person asked to\n be answered and did not ask to be marketed to. Its reads are paid\n only when a merchant presses its own button, so opening a\n submission to read it costs nothing extra.\n */}\n {reader && readerSite ? (\n <SubmissionListAssignment hostId={readerSite} submission={reader} />\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button\n color=\"error\"\n onClick={async () => {\n const target = reader\n setReader(null)\n if (target) await handleDelete(target)()\n }}\n >\n {'Delete'}\n </Button>\n <Button\n onClick={() => {\n if (reader) void handleToggleRead({ ...reader, read: true })()\n setReader(null)\n }}\n >\n {'Mark unread'}\n </Button>\n <Button variant=\"contained\" onClick={() => setReader(null)}>\n {'Close'}\n </Button>\n </DialogActions>\n </Dialog>\n </>\n )\n}\nSubmissionsCard.displayName = 'SubmissionsCard'\n\nexport default SubmissionsCard\n"],"names":["FORMS_MAX_PER_HOST","pluginDocsHelp","INBOX_SUBMISSION_PARAM","FORM_SUBMISSION_KIND","InboxRecordAttributionZone","pluginRecordByEmailHref","pluginRecordListHref","mdiAccountArrowRight","mdiDeleteOutline","mdiEmailOpenOutline","mdiEmailOutline","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","useSnackbar","useFirestore","useFirestoreCollection","usePluginApiPost","useListQuery","Avatar","Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Divider","Stack","Tooltip","Typography","collection","deleteDoc","getDoc","limit","orderBy","query","updateDoc","pluginRecordListDoc","pluginRecordListWalk","useSearchParams","useCallback","useEffect","useMemo","useRef","useState","relativeTime","routingChips","senderHue","submissionSender","FORM_SCOPED_SUBMISSION_LIST_QUERY","ORG_SUBMISSION_LIST_QUERY","SUBMISSION_FILTER_HEADERS","SUBMISSION_LIST_QUERY","SUBMISSION_READ_OPTIONS","formSubmissionBase","orgSubmissionBase","orgSiteName","SubmissionListAssignment","SubmissionReply","useRecordRouteContext","FORM_STATS_API_PATH","SUBMISSION_SELECT_FIELDS","SUBMISSION_HIDDEN_COLUMNS","read","formId","messageTextOf","submission","Object","entries","fields","map","key","value","join","SubmissionsCard","hostId","orgMount","reader","firestore","enqueueSnackbar","postPluginApi","confirm","scoped","Boolean","orgId","siteOf","routeContext","data","formDocs","idField","formsTruncated","length","forms","slice","sort","left","right","String","displayName","$id","localeCompare","offersForm","declaration","filter","field","column","base","gridFilter","selectFields","rows","submissions","hasMore","hasMoreSubmissions","page","submissionPage","setPage","setSubmissionPage","pageSize","submissionPageSize","setPageSize","setSubmissionPageSize","plan","request","clauses","search","searchWords","deps","submissionOptions","form","label","hosts","host","id","name","refusals","refused","headers","options","searching","some","word","trim","filtering","onlyForm","every","clause","setReader","handleOpenReader","site","submissionDoc","handleToggleRead","searchParams","seededSubmissionId","get","seededOpened","current","seededDoc","then","snapshot","exists","variant","persist","catch","undefined","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","formIds","submissionActions","senderEmail","email","contactsHref","icon","path","size","href","disabled","disabledReason","onClick","destructive","submissionColumns","headerName","flex","minWidth","valueGetter","_value","renderCell","row","sender","hue","direction","spacing","sx","alignItems","aria-label","width","height","borderRadius","bgcolor","flexShrink","fontSize","initials","noWrap","formName","createdAt","toDate","getTime","toLocaleString","span","hideable","items","readerSite","header","help","anchor","excerpt","contentGutterX","contentGutterY","contentBordered","component","mb","onChange","setClauses","notices","columns","initialState","columnVisibilityModel","noRowsLabel","rowHeight","onOpen","_id","getRowClassName","fontWeight","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","rowCount","onPageChange","onPageSizeChange","open","onClose","maxWidth","fullWidth","screenId","my","whiteSpace","wordBreak","mt","flexWrap","rowGap","routing","chip","recordKind","recordId","target"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/submissions-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 FORMS_MAX_PER_HOST,\n pluginDocsHelp,\n type ConsolePluginOrgMount,\n} from '@aglyn/aglyn'\nimport { INBOX_SUBMISSION_PARAM } from '../model/inbox-record-routes'\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\nimport { submissionFiledUnder, submissionLinks } from '../model/submission-links'\n\n/** The forms plugin's record kind this card reads (AGL-3080). */\nconst FORM_SUBMISSION_KIND = 'formSubmission'\n// A deep import, NOT the plugin barrel (AGL-1151): the barrel is the entry\n// point the tenant's loader dynamically imports to activate the marketing\n// plugin's SITE half, so a console card named there ships to every published\n// page. The component path reaches the same module without crossing it.\nimport { InboxRecordAttributionZone } from './inbox-attribution-zone'\n// The CRM's route builder by its leaf path, for the reason above: the barrel\n// is the plugin's site entry point.\nimport {\n pluginRecordByEmailHref,\n pluginRecordHref,\n pluginRecordListHref,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport {\n mdiAccountArrowRight,\n mdiDeleteOutline,\n mdiEmailOpenOutline,\n mdiEmailOutline,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport {\n type ListFilterOption,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n usePluginApiPost,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport {\n Avatar,\n Box,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Divider,\n Stack,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n deleteDoc,\n getDoc,\n limit,\n orderBy,\n query,\n updateDoc,\n} from 'firebase/firestore'\n// The submissions are the forms plugin's records, walked and opened through\n// the list source it publishes (AGL-3080).\nimport {\n pluginRecordListDoc,\n pluginRecordListWalk,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport { useSearchParams } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n relativeTime,\n routingChips,\n senderHue,\n submissionSender,\n} from '../model/submission-presenter'\nimport {\n FORM_SCOPED_SUBMISSION_LIST_QUERY,\n ORG_SUBMISSION_LIST_QUERY,\n SUBMISSION_FILTER_HEADERS,\n SUBMISSION_LIST_QUERY,\n SUBMISSION_READ_OPTIONS,\n formSubmissionBase,\n orgSubmissionBase,\n} from '../constants/list-queries'\nimport { orgSiteName } from './inbox-org-sites'\nimport SubmissionListAssignment from './submission-list-assignment.component'\nimport SubmissionReply from './submission-reply.component'\nimport { useRecordRouteContext } from './use-record-route-context'\n\n\n/**\n * The forms plugin's recount route (AGL-3330), by address: this plugin\n * shows the submissions a form counts and may not import the plugin that\n * keeps the counts.\n */\nconst FORM_STATS_API_PATH = '/api/forms/stats'\n/** Read, Form and Site are picked, so the panel shows selects. */\nconst SUBMISSION_SELECT_FIELDS = ['read', 'formId', 'hostId']\nconst SUBMISSION_HIDDEN_COLUMNS = { read: false, formId: false }\n\n/** Every field of a message, as one line — what the Message column draws. */\nconst messageTextOf = (submission: any): string =>\n Object.entries(submission?.fields ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join(' · ')\n\n/**\n * The Submissions section of the Inbox (AGL-77/104/109 → AGL-395): the form\n * messages a site collected, newest first, and the reader that opens one.\n *\n * Its own component since AGL-2501, when the Inbox's tabs became routes. The\n * split is what makes \"mount only the section being read\" structural: hooks\n * cannot be conditional, so a page holding every section's reads pays for all\n * of them whichever one the URL names.\n */\nexport interface SubmissionsCardProps {\n /**\n * The site whose submissions are listed, or `null` on the organization's\n * Inbox, where the card lists every site's at once (AGL-3303) and each row\n * is acted on as the site it was sent to.\n */\n hostId: string | null\n /** The organization and its sites — present exactly when `hostId` is `null`. */\n orgMount?: ConsolePluginOrgMount\n /**\n * Narrow this card to ONE form, permanently.\n *\n * What the forms plugin's detail surface renders instead of a second\n * submissions table. The reader has already chosen the subject by being on\n * that page, so scoping here does three things a copy would have had to\n * re-derive: the form picker is not rendered (there is nothing to pick),\n * the site's `forms` collection is not read at all (the picker was its only\n * consumer), and the empty state names the form rather than the site.\n *\n * Everything else — the paged walk, the reader dialog, read/unread,\n * delete, reply, list assignment, attribution — is the same code answering\n * a narrower query. That is the reason this is a prop rather than a second\n * component: a per-form table written separately would be a second reader\n * to keep in step, and it would have been the one to reintroduce the\n * unordered `limit()` this card's own comment exists to warn about.\n */\n formId?: string\n}\n\nexport function SubmissionsCard({\n hostId,\n formId,\n orgMount,\n}: SubmissionsCardProps) {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const postPluginApi = usePluginApiPost()\n const { confirm } = useConfirmationContext()\n /** Scoped to one form: the subject is fixed and nothing may widen it. */\n const scoped = Boolean(formId)\n /** Every site of this organization at once, when no site is named. */\n const orgId = hostId == null ? (orgMount?.orgId ?? null) : null\n /*\n * The site a row lives on, which is where every act on it is addressed:\n * this card's own site, or — across every site — the `hostId` the submit\n * route stamps on the row. That stamp is frozen against a client update and\n * written from the row's own path, so it names the document's real parent;\n * the rules check the host role on that site either way.\n */\n const siteOf = useCallback(\n (submission: any): string | null =>\n hostId ??\n (typeof submission?.hostId === 'string' && submission.hostId\n ? submission.hostId\n : null),\n [hostId],\n )\n /*\n * Where the sender's CONTACT is (AGL-2612). A submission that carried an\n * address updated a contact in the CRM at stage Lead; the row links to\n * it by that address, and the Contacts list — which holds the id nothing\n * here does — opens the record. The same hub path the Members & leads\n * rows use to open a lead.\n */\n const routeContext = useRecordRouteContext()\n\n /*\n * The site's forms, for the Submissions filter.\n *\n * `FORMS_MAX_PER_HOST` is a read WINDOW, not a cap on the collection — how\n * many forms a site may hold is `formsPerHost`, enforced at the create in\n * `/api/hosts/resources`, and a staff-set per-org override can raise it\n * past this window. So the window can be smaller than the catalog, and one\n * more document than fits is read on purpose: a filter that quietly listed\n * the first N would report \"this form does not exist\" and \"this form is\n * past the window\" as the same empty answer.\n *\n * Ordered by `__name__`. `displayName` would be the nicer order and is the\n * wrong instrument: `orderBy` on a data field DROPS every document missing\n * it, invisibly, so a form saved without a name would vanish from its own\n * filter. The list is sorted for display below, where a missing name is\n * merely ugly.\n */\n const { data: formDocs } = useFirestoreCollection<any>(\n () =>\n // Forms are one site's catalog. Across every site there is no single\n // catalog to filter by, and picking a site is how a reader gets one.\n scoped || !hostId\n ? null\n : query(\n collection(firestore, 'hosts', hostId, 'forms'),\n orderBy('__name__'),\n limit(FORMS_MAX_PER_HOST + 1),\n ),\n [firestore, hostId, scoped],\n { idField: '$id' },\n )\n /** More forms exist than the window shows; the filter has to say so. */\n const formsTruncated = (formDocs?.length ?? 0) > FORMS_MAX_PER_HOST\n const forms = useMemo(\n () =>\n [...(formDocs ?? [])]\n .slice(0, FORMS_MAX_PER_HOST)\n .sort((left: any, right: any) =>\n String(left.displayName ?? left.$id).localeCompare(\n String(right.displayName ?? right.$id),\n ),\n ),\n [formDocs],\n )\n /** Form is offered where the site's catalog is read. */\n const offersForm = !scoped && Boolean(hostId) && forms.length > 0\n /*\n * WHAT THIS CARD'S LIST CAN BE ASKED (AGL-3321) — every clause and the\n * search on the query, see `SUBMISSION_LIST_QUERY`:\n *\n * - a site's Inbox: From, Read and — where the site has forms — Form;\n * - a card scoped to one form: that form is the list's own scope, and\n * Form has nothing left to pick, so it is not offered. The scope wins\n * over any clause: it is the base, and no clause can reach it;\n * - every site's, on the organization's Inbox: From, Read and Site,\n * under the `orgId` clause the rules require of the group read.\n */\n const declaration = useMemo<ListQueryDeclaration>(() => {\n if (scoped) return FORM_SCOPED_SUBMISSION_LIST_QUERY\n if (hostId == null) return ORG_SUBMISSION_LIST_QUERY\n return offersForm\n ? SUBMISSION_LIST_QUERY\n : {\n ...SUBMISSION_LIST_QUERY,\n fields: SUBMISSION_LIST_QUERY.fields.filter((field) => field.column !== 'formId'),\n }\n }, [scoped, hostId, offersForm])\n const base = useMemo(\n () =>\n formId ? formSubmissionBase(formId) : hostId == null && orgId ? orgSubmissionBase(orgId) : [],\n [formId, hostId, orgId],\n )\n const gridFilter = useListGridFilter({ selectFields: SUBMISSION_SELECT_FIELDS })\n\n /*\n * The inbox WALKS its submissions instead of sampling them (AGL-2501,\n * AGL-2292), and every filter and search word is a predicate on the walk\n * (AGL-3321), so a page is a page of the matches.\n *\n * `limit(200)` carried no `orderBy`, so Firestore answered it in\n * DOCUMENT-ID order over ids `add()` generates — an arbitrary two hundred\n * of the site's messages, which the client sort then arranged newest-first\n * so the result looked like a feed. A site past two hundred submissions\n * could not reach the rest, and the messages missing left no gap: the row\n * dates on screen simply skipped, which reads as a quiet week rather than\n * as an unreachable inbox.\n *\n * `createdAt` is safe to order on, checked against the writer rather than\n * assumed: `libs/plugins/forms/src/lib/server/form-submit.ts` is the only path\n * that creates one and stamps `createdAt: serverTimestamp()` on every add,\n * the v1 API only ever reads and deletes, and `formSubmissions` is absent\n * from `IMPORTABLE_FIELDS`, so no restore path can make one without it.\n *\n * EVERY SITE'S SUBMISSIONS IN ONE QUERY on the organization's Inbox\n * (AGL-3303), not a listener per site: the collection group, newest first.\n * The `orgId` clause is not a filter this card could drop. The rules admit\n * a group read only when it is narrowed to an org the reader spans, so an\n * unfiltered group query is refused outright. Every submission carries\n * `orgId`: the submit route stamps it, and the rows written before it did\n * were stamped once (AGL-3303). A row without one would be in no\n * organization's list, though its own site's Inbox lists it either way.\n */\n const {\n rows: submissions,\n hasMore: hasMoreSubmissions,\n page: submissionPage,\n setPage: setSubmissionPage,\n pageSize: submissionPageSize,\n setPageSize: setSubmissionPageSize,\n plan,\n } = useListQuery<any>({\n collection: pluginRecordListWalk(FORM_SUBMISSION_KIND, firestore, { hostId, orgId }),\n declaration,\n request: { clauses: gridFilter.clauses, search: gridFilter.searchWords, base },\n deps: [firestore, hostId, orgId],\n idField: '$id',\n })\n const submissionOptions = useMemo<Readonly<Record<string, readonly ListFilterOption[]>>>(\n () => ({\n read: SUBMISSION_READ_OPTIONS,\n ...(offersForm\n ? {\n formId: forms.map((form: any) => ({\n value: String(form.$id),\n label: String(form.displayName || form.$id),\n })),\n }\n : {}),\n ...(hostId == null\n ? {\n hostId: (orgMount?.hosts ?? []).map((host) => ({\n value: host.id,\n label: host.name || host.id,\n })),\n }\n : {}),\n }),\n [offersForm, forms, hostId, orgMount],\n )\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: declaration.fields,\n headers: SUBMISSION_FILTER_HEADERS,\n options: submissionOptions,\n }),\n [plan.refused, declaration, submissionOptions],\n )\n const searching = gridFilter.searchWords.some((word) => word.trim())\n const filtering = gridFilter.clauses.length > 0 || searching\n /** Narrowed by nothing but a Form clause. */\n const onlyForm =\n !searching &&\n gridFilter.clauses.length > 0 &&\n gridFilter.clauses.every((clause) => clause.field === 'formId')\n\n // Mail reader (AGL-104): opening a submission shows the full message and\n // marks it read.\n const [reader, setReader] = useState<any | null>(null)\n const handleOpenReader = useCallback(\n (submission: any) => () => {\n setReader(submission)\n const site = siteOf(submission)\n const submissionDoc = site\n ? pluginRecordListDoc(FORM_SUBMISSION_KIND, firestore, { hostId: site, id: submission.$id })\n : null\n if (!submission.read && submissionDoc) {\n void updateDoc(submissionDoc, { read: true })\n }\n },\n [firestore, siteOf],\n )\n\n const handleToggleRead = useCallback(\n (submission: any) => () => {\n const site = siteOf(submission)\n const submissionDoc = site\n ? pluginRecordListDoc(FORM_SUBMISSION_KIND, firestore, { hostId: site, id: submission.$id })\n : null\n if (!submissionDoc) return\n void updateDoc(submissionDoc, { read: !submission.read })\n },\n [firestore, siteOf],\n )\n\n /*\n * A submission named in the URL opens in the reader on arrival (AGL-2622).\n *\n * A contact's timeline names the submission that captured the person, and\n * the address it links to is this tab with `?submission={id}`. The\n * document is read once, by id, rather than looked for in the paged\n * window: the window is the newest page of a walk that may not reach a\n * submission from months back, and one document read is what the URL\n * asked for. Opening it marks it read, as a click on the row would. A\n * submission that is gone says so, because a reader that silently stays\n * shut reads as the link having done nothing.\n *\n * Once per id: the ref keeps a re-render — and the same URL re-settling —\n * from reopening a reader the person has since closed. The landing read\n * is judged against that ref rather than an effect cleanup: a listener\n * settling while the read is in flight re-renders this card, and a\n * cleanup keyed on any dep would cancel the answer to the question the\n * URL just asked.\n */\n const searchParams = useSearchParams()\n const seededSubmissionId = searchParams?.get(INBOX_SUBMISSION_PARAM) ?? null\n const seededOpened = useRef<string | null>(null)\n useEffect(() => {\n // A submission link names one site's Inbox; the org's has no site to read\n // the id under.\n if (!hostId) return\n if (!seededSubmissionId || seededOpened.current === seededSubmissionId) return\n seededOpened.current = seededSubmissionId\n const seededDoc = pluginRecordListDoc(FORM_SUBMISSION_KIND, firestore, {\n hostId,\n id: seededSubmissionId,\n })\n if (!seededDoc) return\n void getDoc(seededDoc)\n .then((snapshot) => {\n if (seededOpened.current !== snapshot.id) return\n if (!snapshot.exists()) {\n enqueueSnackbar('That submission is no longer in the Inbox.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n handleOpenReader({ ...snapshot.data(), $id: snapshot.id })()\n })\n .catch(() => undefined)\n }, [seededSubmissionId, firestore, hostId, handleOpenReader, enqueueSnackbar])\n\n const handleDelete = useCallback(\n (submission: any) => async () => {\n const site = siteOf(submission)\n const submissionDoc = site\n ? pluginRecordListDoc(FORM_SUBMISSION_KIND, firestore, { hostId: site, id: submission.$id })\n : null\n if (!site || !submissionDoc) return\n const confirmed = await confirm({\n title: 'Delete this submission?',\n description: 'The submission is removed permanently.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await deleteDoc(submissionDoc)\n enqueueSnackbar('Submission deleted', {\n variant: 'success',\n persist: false,\n })\n /*\n * The form counted this submission when it arrived (AGL-3330), and an\n * increment cannot see a delete: the forms plugin recounts the form\n * from the rows that are left. Best effort — the delete has happened\n * either way, and the recount script catches a follow-up that failed.\n */\n if (typeof submission.formId === 'string' && submission.formId) {\n void postPluginApi(FORM_STATS_API_PATH, { hostId: site, formIds: [submission.formId] })\n }\n },\n [confirm, firestore, siteOf, enqueueSnackbar, postPluginApi],\n )\n\n /*\n * One row per submission, newest first: who sent it, what it said and how\n * long ago. The row opens the reader, which also marks it read.\n */\n const submissionActions = (submission: any): RowActionsMenuItem[] => {\n const senderEmail = submissionSender(submission.fields).email\n // Where the sender's contact is read, asked of whichever plugin keeps\n // people; with none loaded there is no such page, and no menu item.\n const contactsHref = routeContext\n ? pluginRecordListHref('contact', routeContext)\n : null\n return [\n ...(routeContext && contactsHref\n ? [\n {\n key: 'crm',\n label: 'Open contact in CRM',\n icon: <MdiIcon path={mdiAccountArrowRight.path} size={0.8} />,\n ...(senderEmail\n ? {\n href:\n pluginRecordByEmailHref(\n 'contact',\n routeContext,\n senderEmail,\n ) ?? contactsHref,\n }\n : {\n disabled: true,\n disabledReason:\n 'This submission carried no email address, so no contact was updated.',\n }),\n },\n ]\n : []),\n {\n key: 'read',\n label: submission.read ? 'Mark unread' : 'Mark read',\n icon: (\n <MdiIcon\n path={submission.read ? mdiEmailOutline.path : mdiEmailOpenOutline.path}\n size={0.8}\n />\n ),\n onClick: handleToggleRead(submission),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: handleDelete(submission),\n },\n ]\n }\n const submissionColumns: GridColDef[] = [\n {\n /*\n The mockup's list is people, not forms (AGL-2168): an initials avatar,\n the sender, and the form name beneath it. The unread DOT replaces the\n `New` chip — the row is already bold, and a chip that says \"New\" beside\n bold text is the same fact twice.\n */\n field: 'from',\n headerName: 'From',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, submission) => submissionSender(submission.fields).label,\n renderCell: ({ row: submission }) => {\n const sender = submissionSender(submission.fields)\n const hue = senderHue(sender.label)\n return (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', minWidth: 0 }}>\n {!submission.read ? (\n <Box\n aria-label=\"Unread\"\n sx={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n bgcolor: 'primary.main',\n flexShrink: 0,\n }}\n />\n ) : (\n <Box sx={{ width: 8, flexShrink: 0 }} />\n )}\n <Avatar\n sx={{\n width: 28,\n height: 28,\n fontSize: 13,\n bgcolor: `hsl(${hue} 55% 45%)`,\n }}\n >\n {sender.initials}\n </Avatar>\n <Stack sx={{ 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 </Stack>\n )\n },\n },\n /*\n Which site it was sent to, on the organization's Inbox only — under a\n site every row is that site's, and a column saying so would be noise.\n */\n ...(hostId == null\n ? [\n {\n field: 'hostId',\n headerName: 'Site',\n flex: 1,\n minWidth: 140,\n // Sorted and drawn by name; the filter matches the id.\n valueGetter: (_value: unknown, submission: any) =>\n orgSiteName(orgMount, submission.hostId),\n renderCell: ({ row: submission }: { row: any }) =>\n orgSiteName(orgMount, submission.hostId),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'message',\n headerName: 'Message',\n flex: 2,\n minWidth: 260,\n valueGetter: (_value, submission) => messageTextOf(submission),\n renderCell: ({ value }) => (\n <Typography variant=\"body2\" noWrap>\n {value}\n </Typography>\n ),\n },\n {\n /*\n Relative, as the mockup shows it — an inbox is scanned for recency and a\n locale timestamp makes the reader do the subtraction. The absolute time\n stays on the detail dialog, where it is the fact you actually want.\n */\n field: 'createdAt',\n headerName: 'Received',\n width: 130,\n valueGetter: (_value, submission) =>\n submission.createdAt?.toDate?.()?.getTime?.() ?? 0,\n renderCell: ({ row: submission }) => (\n <Tooltip title={submission.createdAt?.toDate?.().toLocaleString() ?? ''}>\n <span>{relativeTime(submission.createdAt?.toDate?.().getTime())}</span>\n </Tooltip>\n ),\n },\n /*\n READ, as the panel picks it — never drawn: the unread dot and the bold\n row already say it. A boolean field has no hidden column of its own (the\n grid's boolean operators are not the select's), so it is declared here,\n kept hidden and out of Manage columns.\n */\n {\n field: 'read',\n headerName: 'Read',\n hideable: false,\n valueGetter: (_value, submission) => String(Boolean(submission.read)),\n },\n /*\n One overflow menu, as the Members & leads rows have: two inline buttons\n had no room for a third, and the contact link is the one a reader\n reaches for after reading. Present but disabled for a submission with no\n address, with the reason — a row that simply lacked the item would read\n as the contact not existing.\n */\n listActionsColumn(\n (submission) => (\n <ListRowActions\n label={String(submissionSender(submission.fields).email ?? submission.$id)}\n items={submissionActions(submission)}\n />\n ),\n { width: 72 },\n ),\n ]\n\n /** The site the open submission was sent to, which its reader acts as. */\n const readerSite = reader ? siteOf(reader) : null\n\n return (\n <>\n <CardDisplay\n header={scoped ? 'Submissions to this form' : 'Form Submissions'}\n help={pluginDocsHelp(\n 'forms',\n hostId == null\n ? {\n anchor: '#every-sites-inbox-at-once',\n excerpt:\n 'Every site’s form messages in one list, newest first, ' +\n 'each under the site it was sent to.',\n }\n : {\n anchor: '#the-inbox',\n excerpt:\n 'Messages your forms collected, newest first, showing who ' +\n 'sent each one and where it was routed.',\n },\n )}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n {/*\n * The Inbox stays the site-wide answer to \"who is waiting for a\n * reply\" — that question does not decompose by form. The panel's\n * Form filter narrows it on request; it does not turn the page into\n * a per-form view. It is offered only when the site HAS forms, and\n * never on a card already scoped to one.\n */}\n {offersForm && formsTruncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\" sx={{ mb: 1 }}>\n {`The Form filter offers the first ${FORMS_MAX_PER_HOST.toLocaleString()} ` +\n 'forms. Narrow by form is incomplete; with no Form filter the ' +\n 'list still covers every submission.'}\n </Typography>\n ) : null}\n {hostId == null ? (\n <Typography\n variant=\"body2\"\n color=\"text.secondary\"\n sx={{ mb: submissions.length ? 2 : 1 }}\n >\n {'Every site’s submissions, newest first. Choose a site to ' +\n 'narrow them by form.'}\n </Typography>\n ) : null}\n {submissions.length === 0 && !hasMoreSubmissions && !filtering ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {scoped\n ? 'No submissions carry this form’s id yet. Messages this ' +\n 'form’s design collected before it became a form entity are ' +\n 'in the Inbox, filed under the name they were sent with.'\n : hostId == null\n ? 'No form submissions on any site yet.'\n : 'No form submissions yet. Add a Contact Form element to a ' +\n 'page — visitor messages arrive here.'}\n </Typography>\n ) : (\n <>\n <ListFilterChips\n fields={declaration.fields}\n headers={SUBMISSION_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={submissionOptions}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label={scoped ? 'Submissions to this form' : 'Form submissions'}\n rows={submissions}\n columns={listFilterGridColumns(\n submissionColumns,\n declaration.fields,\n submissionOptions,\n SUBMISSION_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: SUBMISSION_HIDDEN_COLUMNS } }}\n noRowsLabel={\n onlyForm\n ? 'No submissions for this form yet. Submissions sent before ' +\n 'the form was created are listed with no Form filter.'\n : 'No submissions match these filters'\n }\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, submission) => handleOpenReader(submission)()}\n // An unread submission reads bold across its row, which is why\n // the list needs no `New` chip.\n getRowClassName={({ row }) => (row.read ? '' : 'submission-unread')}\n sx={{\n '& .submission-unread .MuiDataGrid-cell, & .submission-unread .MuiTypography-root':\n { fontWeight: 'fontWeightBold' },\n }}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to the query; the grid neither\n // filters nor sorts the page it holds.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n />\n <ListPagination\n page={submissionPage}\n pageSize={submissionPageSize}\n rowCount={submissions.length}\n hasMore={hasMoreSubmissions}\n onPageChange={setSubmissionPage}\n onPageSizeChange={setSubmissionPageSize}\n />\n </>\n )}\n </CardDisplay>\n <Dialog\n open={Boolean(reader)}\n onClose={() => setReader(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{reader?.formName ?? 'Form submission'}</DialogTitle>\n <DialogContent>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Received ${reader?.createdAt?.toDate?.().toLocaleString() ?? ''}` +\n // The site it was sent to, where the list spans every site.\n (hostId == null && readerSite\n ? ` · ${orgSiteName(orgMount, readerSite)}`\n : '')}\n </Typography>\n {/*\n WHERE IT CAME IN, AND WHAT IT MADE (AGL-3461) — the form, the page\n it was on, and the lead or contact it filed, each a link to its\n own page. The page is also the field the marketing console's\n landing-page grouping joins on, so a reader can check one row\n against that grouping. Each link is asked of the plugin that owns\n the record, and is text where no plugin answers.\n */}\n {reader ? (\n <Stack spacing={0.5} sx={{ mt: 1 }}>\n {submissionLinks({\n submission: reader,\n hrefOf: (kind, id) =>\n routeContext ? pluginRecordHref(kind, routeContext, id) : null,\n siteOrigin:\n hostPublicOrigin({\n subdomain:\n (hostId != null ? routeContext?.host : null) ??\n orgMount?.hosts.find((site) => site.id === readerSite)?.subdomain ??\n null,\n }) ?? null,\n }).map((link) => (\n <Stack key={link.key} direction=\"row\" spacing={1} sx={{ alignItems: 'baseline' }}>\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{ minWidth: 72 }}>\n {link.label}\n </Typography>\n {link.href ? (\n <AppLink\n href={link.href}\n {...(link.external ? { target: '_blank', rel: 'noopener' } : {})}\n >\n {link.text}\n </AppLink>\n ) : (\n <Typography variant=\"body2\">{link.text}</Typography>\n )}\n </Stack>\n ))}\n </Stack>\n ) : null}\n <Divider sx={{ my: 1.5 }} />\n <Stack spacing={1.5}>\n {Object.entries(reader?.fields ?? {}).map(([key, value]) => (\n <Stack key={key} spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {key}\n </Typography>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n >\n {String(value)}\n </Typography>\n </Stack>\n ))}\n </Stack>\n {/*\n What happened to this submission (AGL-2168). The mockup puts\n these under the fields: `Saved to Inbox` and `Added to \"Leads\"\n dataset`. The second is stamped by the submit route only when a\n record was really appended — a form bound to a deleted dataset,\n or one whose record quota is full, shows no chip rather than a\n chip for a row that does not exist. Both are failures the route\n already swallows silently, and a chip that lied about them\n would be worse than the silence.\n */}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ mt: 2, flexWrap: 'wrap', rowGap: 1 }}\n >\n {routingChips(reader?.routing).map((chip) => (\n <Chip\n key={chip.label}\n size=\"small\"\n label={chip.label}\n color={chip.color}\n variant=\"outlined\"\n />\n ))}\n </Stack>\n {/*\n WHERE THIS SUBMISSION CAME FROM.\n\n One keyed document read — the attribution's id is `form:{id}` and\n the submission's id is that `{id}` — paid when a merchant opens a\n submission rather than once per row of the list. A submission that\n was credited to nobody renders the sentence saying so; it never\n renders a campaign with a zero beside it.\n */}\n {reader?.$id && readerSite ? (\n <Box sx={{ mt: 2 }}>\n <InboxRecordAttributionZone\n hostId={readerSite}\n recordKind=\"form\"\n recordId={String(reader.$id)}\n filedUnder={submissionFiledUnder(reader)}\n />\n </Box>\n ) : null}\n {/*\n Answering the person is the act the Inbox exists for, and it is\n inside the reader rather than on the row because a reply written\n without the message in front of you is the reply that answers the\n wrong question. Mounted with the dialog, so its reads — the site\n name and the replies already sent — are paid when a merchant opens\n a submission and not once per visit to this page.\n */}\n {reader && readerSite ? (\n <SubmissionReply hostId={readerSite} submission={reader} />\n ) : null}\n {/*\n Enrolling the sender in a marketing list — a SEPARATE act from\n answering them, and a separate card, because the person asked to\n be answered and did not ask to be marketed to. Its reads are paid\n only when a merchant presses its own button, so opening a\n submission to read it costs nothing extra.\n */}\n {reader && readerSite ? (\n <SubmissionListAssignment hostId={readerSite} submission={reader} />\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button\n color=\"error\"\n onClick={async () => {\n const target = reader\n setReader(null)\n if (target) await handleDelete(target)()\n }}\n >\n {'Delete'}\n </Button>\n <Button\n onClick={() => {\n if (reader) void handleToggleRead({ ...reader, read: true })()\n setReader(null)\n }}\n >\n {'Mark unread'}\n </Button>\n <Button variant=\"contained\" onClick={() => setReader(null)}>\n {'Close'}\n </Button>\n </DialogActions>\n </Dialog>\n </>\n )\n}\nSubmissionsCard.displayName = 'SubmissionsCard'\n\nexport default SubmissionsCard\n"],"names":["FORMS_MAX_PER_HOST","pluginDocsHelp","INBOX_SUBMISSION_PARAM","hostPublicOrigin","submissionFiledUnder","submissionLinks","FORM_SUBMISSION_KIND","InboxRecordAttributionZone","pluginRecordByEmailHref","pluginRecordHref","pluginRecordListHref","mdiAccountArrowRight","mdiDeleteOutline","mdiEmailOpenOutline","mdiEmailOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","useSnackbar","useFirestore","useFirestoreCollection","usePluginApiPost","useListQuery","Avatar","Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Divider","Stack","Tooltip","Typography","collection","deleteDoc","getDoc","limit","orderBy","query","updateDoc","pluginRecordListDoc","pluginRecordListWalk","useSearchParams","useCallback","useEffect","useMemo","useRef","useState","relativeTime","routingChips","senderHue","submissionSender","FORM_SCOPED_SUBMISSION_LIST_QUERY","ORG_SUBMISSION_LIST_QUERY","SUBMISSION_FILTER_HEADERS","SUBMISSION_LIST_QUERY","SUBMISSION_READ_OPTIONS","formSubmissionBase","orgSubmissionBase","orgSiteName","SubmissionListAssignment","SubmissionReply","useRecordRouteContext","FORM_STATS_API_PATH","SUBMISSION_SELECT_FIELDS","SUBMISSION_HIDDEN_COLUMNS","read","formId","messageTextOf","submission","Object","entries","fields","map","key","value","join","SubmissionsCard","hostId","orgMount","reader","firestore","enqueueSnackbar","postPluginApi","confirm","scoped","Boolean","orgId","siteOf","routeContext","data","formDocs","idField","formsTruncated","length","forms","slice","sort","left","right","String","displayName","$id","localeCompare","offersForm","declaration","filter","field","column","base","gridFilter","selectFields","rows","submissions","hasMore","hasMoreSubmissions","page","submissionPage","setPage","setSubmissionPage","pageSize","submissionPageSize","setPageSize","setSubmissionPageSize","plan","request","clauses","search","searchWords","deps","submissionOptions","form","label","hosts","host","id","name","refusals","refused","headers","options","searching","some","word","trim","filtering","onlyForm","every","clause","setReader","handleOpenReader","site","submissionDoc","handleToggleRead","searchParams","seededSubmissionId","get","seededOpened","current","seededDoc","then","snapshot","exists","variant","persist","catch","undefined","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","formIds","submissionActions","senderEmail","email","contactsHref","icon","path","size","href","disabled","disabledReason","onClick","destructive","submissionColumns","headerName","flex","minWidth","valueGetter","_value","renderCell","row","sender","hue","direction","spacing","sx","alignItems","aria-label","width","height","borderRadius","bgcolor","flexShrink","fontSize","initials","noWrap","formName","createdAt","toDate","getTime","toLocaleString","span","hideable","items","readerSite","header","help","anchor","excerpt","contentGutterX","contentGutterY","contentBordered","component","mb","onChange","setClauses","notices","columns","initialState","columnVisibilityModel","noRowsLabel","rowHeight","onOpen","_id","getRowClassName","fontWeight","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","rowCount","onPageChange","onPageSizeChange","open","onClose","maxWidth","fullWidth","mt","hrefOf","kind","siteOrigin","subdomain","find","link","external","target","rel","text","my","whiteSpace","wordBreak","flexWrap","rowGap","routing","chip","recordKind","recordId","filedUnder"],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* WHAT AN OPEN SUBMISSION LINKS TO (AGL-3461).
|
|
19
|
+
*
|
|
20
|
+
* A submission names the form that took it, the page it was on and — since
|
|
21
|
+
* the submit route stamps it — the lead or contact the record system filed
|
|
22
|
+
* for its sender. Each is somebody else's record: the form is the Forms
|
|
23
|
+
* plugin's, the person the CRM's, and the page is the live site. So each
|
|
24
|
+
* link is asked of the owner (`hrefOf`, the record-route registry) and falls
|
|
25
|
+
* back to plain text when no plugin answers, rather than to a URL this
|
|
26
|
+
* plugin spelled for them.
|
|
27
|
+
*/
|
|
28
|
+
/** The record the submit route stamps once the person is filed. */
|
|
29
|
+
export interface SubmissionCapturedRecord {
|
|
30
|
+
kind?: string;
|
|
31
|
+
id?: string;
|
|
32
|
+
}
|
|
33
|
+
/** The fields of a submission row this module reads. */
|
|
34
|
+
export interface SubmissionLinkSource {
|
|
35
|
+
formId?: unknown;
|
|
36
|
+
formName?: unknown;
|
|
37
|
+
path?: unknown;
|
|
38
|
+
campaignIds?: unknown;
|
|
39
|
+
pageCampaignIds?: unknown;
|
|
40
|
+
capturedRecord?: SubmissionCapturedRecord | null;
|
|
41
|
+
}
|
|
42
|
+
/** One line of the reader's "where it came in" block. */
|
|
43
|
+
export interface SubmissionLink {
|
|
44
|
+
key: 'form' | 'page' | 'record';
|
|
45
|
+
/** What the line is: `Form`, `Page`, `Lead`, `Contact`. */
|
|
46
|
+
label: string;
|
|
47
|
+
/** What the link reads. */
|
|
48
|
+
text: string;
|
|
49
|
+
/** Where it goes, or `null` to draw `text` plainly. */
|
|
50
|
+
href: string | null;
|
|
51
|
+
/** The live site, which opens beside the console rather than over it. */
|
|
52
|
+
external?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The submission's form, page and filed person, in that order, each with the
|
|
56
|
+
* link its owner publishes. A line whose fact the row does not carry is
|
|
57
|
+
* left out rather than drawn empty.
|
|
58
|
+
*/
|
|
59
|
+
export declare function submissionLinks(options: {
|
|
60
|
+
submission: SubmissionLinkSource;
|
|
61
|
+
/** The record-route registry's answer for one record, or `null`. */
|
|
62
|
+
hrefOf: (kind: string, id: string) => string | null;
|
|
63
|
+
/** The site's public origin, `https://…`, or `null` when unknown. */
|
|
64
|
+
siteOrigin: string | null;
|
|
65
|
+
}): SubmissionLink[];
|
|
66
|
+
/**
|
|
67
|
+
* The campaigns a submission is FILED under: its form's, then its page's,
|
|
68
|
+
* deduped — the two stamps the submit route writes when it arrives.
|
|
69
|
+
*/
|
|
70
|
+
export declare function submissionFiledUnder(submission: SubmissionLinkSource | null | undefined): string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
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 { normalizeContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
|
|
17
|
+
/** The record kinds a captured person may be filed as. */ const PERSON_KINDS = {
|
|
18
|
+
lead: 'Lead',
|
|
19
|
+
contact: 'Contact'
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The submission's form, page and filed person, in that order, each with the
|
|
23
|
+
* link its owner publishes. A line whose fact the row does not carry is
|
|
24
|
+
* left out rather than drawn empty.
|
|
25
|
+
*/ export function submissionLinks(options) {
|
|
26
|
+
const { submission, hrefOf, siteOrigin } = options;
|
|
27
|
+
const links = [];
|
|
28
|
+
const formId = typeof submission.formId === 'string' ? submission.formId : '';
|
|
29
|
+
const formName = typeof submission.formName === 'string' && submission.formName.trim() ? submission.formName.trim() : 'Form';
|
|
30
|
+
if (formId || typeof submission.formName === 'string') {
|
|
31
|
+
links.push({
|
|
32
|
+
key: 'form',
|
|
33
|
+
label: 'Form',
|
|
34
|
+
text: formName,
|
|
35
|
+
href: formId ? hrefOf('form', formId) : null
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const path = typeof submission.path === 'string' ? submission.path.trim() : '';
|
|
39
|
+
if (path) {
|
|
40
|
+
// Only a site-relative path is joined to the origin; anything else is a
|
|
41
|
+
// value somebody else wrote, and is shown rather than followed.
|
|
42
|
+
const relative = path.startsWith('/') && !path.startsWith('//');
|
|
43
|
+
links.push({
|
|
44
|
+
key: 'page',
|
|
45
|
+
label: 'Page',
|
|
46
|
+
text: path,
|
|
47
|
+
href: relative && siteOrigin ? `${siteOrigin}${path}` : null,
|
|
48
|
+
external: true
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const captured = submission.capturedRecord;
|
|
52
|
+
const kind = typeof (captured == null ? void 0 : captured.kind) === 'string' ? captured.kind : '';
|
|
53
|
+
const id = typeof (captured == null ? void 0 : captured.id) === 'string' ? captured.id : '';
|
|
54
|
+
if (PERSON_KINDS[kind] && id) {
|
|
55
|
+
links.push({
|
|
56
|
+
key: 'record',
|
|
57
|
+
label: PERSON_KINDS[kind],
|
|
58
|
+
text: kind === 'lead' ? 'Open the lead' : 'Open the contact',
|
|
59
|
+
href: hrefOf(kind, id)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return links;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The campaigns a submission is FILED under: its form's, then its page's,
|
|
66
|
+
* deduped — the two stamps the submit route writes when it arrives.
|
|
67
|
+
*/ export function submissionFiledUnder(submission) {
|
|
68
|
+
return normalizeContainerIds([
|
|
69
|
+
...Array.isArray(submission == null ? void 0 : submission.campaignIds) ? submission.campaignIds : [],
|
|
70
|
+
...Array.isArray(submission == null ? void 0 : submission.pageCampaignIds) ? submission.pageCampaignIds : []
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=submission-links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/model/submission-links.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 { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\n\n/**\n * WHAT AN OPEN SUBMISSION LINKS TO (AGL-3461).\n *\n * A submission names the form that took it, the page it was on and — since\n * the submit route stamps it — the lead or contact the record system filed\n * for its sender. Each is somebody else's record: the form is the Forms\n * plugin's, the person the CRM's, and the page is the live site. So each\n * link is asked of the owner (`hrefOf`, the record-route registry) and falls\n * back to plain text when no plugin answers, rather than to a URL this\n * plugin spelled for them.\n */\n\n/** The record the submit route stamps once the person is filed. */\nexport interface SubmissionCapturedRecord {\n kind?: string\n id?: string\n}\n\n/** The fields of a submission row this module reads. */\nexport interface SubmissionLinkSource {\n formId?: unknown\n formName?: unknown\n path?: unknown\n campaignIds?: unknown\n pageCampaignIds?: unknown\n capturedRecord?: SubmissionCapturedRecord | null\n}\n\n/** One line of the reader's \"where it came in\" block. */\nexport interface SubmissionLink {\n key: 'form' | 'page' | 'record'\n /** What the line is: `Form`, `Page`, `Lead`, `Contact`. */\n label: string\n /** What the link reads. */\n text: string\n /** Where it goes, or `null` to draw `text` plainly. */\n href: string | null\n /** The live site, which opens beside the console rather than over it. */\n external?: boolean\n}\n\n/** The record kinds a captured person may be filed as. */\nconst PERSON_KINDS: Record<string, string> = { lead: 'Lead', contact: 'Contact' }\n\n/**\n * The submission's form, page and filed person, in that order, each with the\n * link its owner publishes. A line whose fact the row does not carry is\n * left out rather than drawn empty.\n */\nexport function submissionLinks(options: {\n submission: SubmissionLinkSource\n /** The record-route registry's answer for one record, or `null`. */\n hrefOf: (kind: string, id: string) => string | null\n /** The site's public origin, `https://…`, or `null` when unknown. */\n siteOrigin: string | null\n}): SubmissionLink[] {\n const { submission, hrefOf, siteOrigin } = options\n const links: SubmissionLink[] = []\n const formId = typeof submission.formId === 'string' ? submission.formId : ''\n const formName =\n typeof submission.formName === 'string' && submission.formName.trim()\n ? submission.formName.trim()\n : 'Form'\n if (formId || typeof submission.formName === 'string') {\n links.push({\n key: 'form',\n label: 'Form',\n text: formName,\n href: formId ? hrefOf('form', formId) : null,\n })\n }\n const path = typeof submission.path === 'string' ? submission.path.trim() : ''\n if (path) {\n // Only a site-relative path is joined to the origin; anything else is a\n // value somebody else wrote, and is shown rather than followed.\n const relative = path.startsWith('/') && !path.startsWith('//')\n links.push({\n key: 'page',\n label: 'Page',\n text: path,\n href: relative && siteOrigin ? `${siteOrigin}${path}` : null,\n external: true,\n })\n }\n const captured = submission.capturedRecord\n const kind = typeof captured?.kind === 'string' ? captured.kind : ''\n const id = typeof captured?.id === 'string' ? captured.id : ''\n if (PERSON_KINDS[kind] && id) {\n links.push({\n key: 'record',\n label: PERSON_KINDS[kind],\n text: kind === 'lead' ? 'Open the lead' : 'Open the contact',\n href: hrefOf(kind, id),\n })\n }\n return links\n}\n\n/**\n * The campaigns a submission is FILED under: its form's, then its page's,\n * deduped — the two stamps the submit route writes when it arrives.\n */\nexport function submissionFiledUnder(submission: SubmissionLinkSource | null | undefined): string[] {\n return normalizeContainerIds([\n ...(Array.isArray(submission?.campaignIds) ? submission.campaignIds : []),\n ...(Array.isArray(submission?.pageCampaignIds) ? submission.pageCampaignIds : []),\n ])\n}\n"],"names":["normalizeContainerIds","PERSON_KINDS","lead","contact","submissionLinks","options","submission","hrefOf","siteOrigin","links","formId","formName","trim","push","key","label","text","href","path","relative","startsWith","external","captured","capturedRecord","kind","id","submissionFiledUnder","Array","isArray","campaignIds","pageCampaignIds"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,8CAA6C;AA2CnF,wDAAwD,GACxD,MAAMC,eAAuC;IAAEC,MAAM;IAAQC,SAAS;AAAU;AAEhF;;;;CAIC,GACD,OAAO,SAASC,gBAAgBC,OAM/B;IACC,MAAM,EAAEC,UAAU,EAAEC,MAAM,EAAEC,UAAU,EAAE,GAAGH;IAC3C,MAAMI,QAA0B,EAAE;IAClC,MAAMC,SAAS,OAAOJ,WAAWI,MAAM,KAAK,WAAWJ,WAAWI,MAAM,GAAG;IAC3E,MAAMC,WACJ,OAAOL,WAAWK,QAAQ,KAAK,YAAYL,WAAWK,QAAQ,CAACC,IAAI,KAC/DN,WAAWK,QAAQ,CAACC,IAAI,KACxB;IACN,IAAIF,UAAU,OAAOJ,WAAWK,QAAQ,KAAK,UAAU;QACrDF,MAAMI,IAAI,CAAC;YACTC,KAAK;YACLC,OAAO;YACPC,MAAML;YACNM,MAAMP,SAASH,OAAO,QAAQG,UAAU;QAC1C;IACF;IACA,MAAMQ,OAAO,OAAOZ,WAAWY,IAAI,KAAK,WAAWZ,WAAWY,IAAI,CAACN,IAAI,KAAK;IAC5E,IAAIM,MAAM;QACR,wEAAwE;QACxE,gEAAgE;QAChE,MAAMC,WAAWD,KAAKE,UAAU,CAAC,QAAQ,CAACF,KAAKE,UAAU,CAAC;QAC1DX,MAAMI,IAAI,CAAC;YACTC,KAAK;YACLC,OAAO;YACPC,MAAME;YACND,MAAME,YAAYX,aAAa,GAAGA,aAAaU,MAAM,GAAG;YACxDG,UAAU;QACZ;IACF;IACA,MAAMC,WAAWhB,WAAWiB,cAAc;IAC1C,MAAMC,OAAO,QAAOF,4BAAAA,SAAUE,IAAI,MAAK,WAAWF,SAASE,IAAI,GAAG;IAClE,MAAMC,KAAK,QAAOH,4BAAAA,SAAUG,EAAE,MAAK,WAAWH,SAASG,EAAE,GAAG;IAC5D,IAAIxB,YAAY,CAACuB,KAAK,IAAIC,IAAI;QAC5BhB,MAAMI,IAAI,CAAC;YACTC,KAAK;YACLC,OAAOd,YAAY,CAACuB,KAAK;YACzBR,MAAMQ,SAAS,SAAS,kBAAkB;YAC1CP,MAAMV,OAAOiB,MAAMC;QACrB;IACF;IACA,OAAOhB;AACT;AAEA;;;CAGC,GACD,OAAO,SAASiB,qBAAqBpB,UAAmD;IACtF,OAAON,sBAAsB;WACvB2B,MAAMC,OAAO,CAACtB,8BAAAA,WAAYuB,WAAW,IAAIvB,WAAWuB,WAAW,GAAG,EAAE;WACpEF,MAAMC,OAAO,CAACtB,8BAAAA,WAAYwB,eAAe,IAAIxB,WAAWwB,eAAe,GAAG,EAAE;KACjF;AACH"}
|