@aglyn/plugins-inbox 1.0.0-beta.179 → 1.0.0-beta.181

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +10 -10
  2. package/src/lib/components/contacts-card.component.d.ts +10 -2
  3. package/src/lib/components/contacts-card.component.js +94 -41
  4. package/src/lib/components/contacts-card.component.js.map +1 -1
  5. package/src/lib/components/inbox-attribution-zone.d.ts +9 -1
  6. package/src/lib/components/inbox-attribution-zone.js +1 -1
  7. package/src/lib/components/inbox-attribution-zone.js.map +1 -1
  8. package/src/lib/components/inbox-console-page.d.ts +4 -0
  9. package/src/lib/components/inbox-console-page.js +119 -13
  10. package/src/lib/components/inbox-console-page.js.map +1 -1
  11. package/src/lib/components/inbox-console-sections.d.ts +13 -0
  12. package/src/lib/components/inbox-console-sections.js +18 -3
  13. package/src/lib/components/inbox-console-sections.js.map +1 -1
  14. package/src/lib/components/inbox-glance-card.component.js +6 -1
  15. package/src/lib/components/inbox-glance-card.component.js.map +1 -1
  16. package/src/lib/components/inbox-org-sites.d.ts +27 -0
  17. package/src/lib/components/inbox-org-sites.js +31 -0
  18. package/src/lib/components/inbox-org-sites.js.map +1 -0
  19. package/src/lib/components/submissions-card.component.d.ts +10 -2
  20. package/src/lib/components/submissions-card.component.js +89 -29
  21. package/src/lib/components/submissions-card.component.js.map +1 -1
  22. package/src/lib/components/use-inbox-site-pick.d.ts +38 -0
  23. package/src/lib/components/use-inbox-site-pick.js +99 -0
  24. package/src/lib/components/use-inbox-site-pick.js.map +1 -0
  25. package/src/lib/components/use-record-route-context.d.ts +8 -3
  26. package/src/lib/components/use-record-route-context.js +10 -6
  27. package/src/lib/components/use-record-route-context.js.map +1 -1
  28. package/src/lib/plugin.d.ts +4 -3
  29. package/src/lib/plugin.js +35 -5
  30. package/src/lib/plugin.js.map +1 -1
@@ -33,20 +33,31 @@ import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
33
33
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
34
34
  import { useFirestore, useFirestoreCollection, usePagedCollection } from "@aglyn/tenant-feature-instance";
35
35
  import { Avatar, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
36
- import { collection, deleteDoc, doc, getDoc, limit, orderBy, query, updateDoc, where } from "firebase/firestore";
36
+ import { collection, collectionGroup, deleteDoc, doc, getDoc, limit, orderBy, query, updateDoc, where } from "firebase/firestore";
37
37
  import { useSearchParams } from "next/navigation";
38
38
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
39
39
  import { relativeTime, routingChips, senderHue, submissionSender } from "../model/submission-presenter.js";
40
+ import { orgSiteName } from "./inbox-org-sites.js";
40
41
  import SubmissionListAssignment from "./submission-list-assignment.component.js";
41
42
  import SubmissionReply from "./submission-reply.component.js";
42
43
  import { useRecordRouteContext } from "./use-record-route-context.js";
43
- export function SubmissionsCard({ hostId, formId }) {
44
- var _ref, _ref1, _ref2, _ref3, _ref4;
44
+ export function SubmissionsCard({ hostId, formId, orgMount }) {
45
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5;
45
46
  var _reader_createdAt_toDate, _reader_createdAt;
46
47
  const firestore = useFirestore();
47
48
  const { enqueueSnackbar } = useSnackbar();
48
49
  const { confirm } = useConfirmationContext();
49
50
  /** Scoped to one form: the subject is fixed and nothing may widen it. */ const scoped = Boolean(formId);
51
+ /** Every site of this organization at once, when no site is named. */ const orgId = hostId == null ? (_ref = orgMount == null ? void 0 : orgMount.orgId) != null ? _ref : null : null;
52
+ /*
53
+ * The site a row lives on, which is where every act on it is addressed:
54
+ * this card's own site, or — across every site — the `hostId` the submit
55
+ * route stamps on the row. That stamp is frozen against a client update and
56
+ * written from the row's own path, so it names the document's real parent;
57
+ * the rules check the host role on that site either way.
58
+ */ const siteOf = useCallback((submission)=>hostId != null ? hostId : typeof (submission == null ? void 0 : submission.hostId) === 'string' && submission.hostId ? submission.hostId : null, [
59
+ hostId
60
+ ]);
50
61
  /*
51
62
  * Where the sender's CONTACT is (AGL-2612). A submission that carried an
52
63
  * address updated a contact in the CRM at stage Lead; the row links to
@@ -70,14 +81,16 @@ export function SubmissionsCard({ hostId, formId }) {
70
81
  * it, invisibly, so a form saved without a name would vanish from its own
71
82
  * filter. The list is sorted for display below, where a missing name is
72
83
  * merely ugly.
73
- */ const { data: formDocs } = useFirestoreCollection(()=>scoped ? null : query(collection(firestore, 'hosts', hostId, 'forms'), orderBy('__name__'), limit(FORMS_MAX_PER_HOST + 1)), [
84
+ */ const { data: formDocs } = useFirestoreCollection(()=>// Forms are one site's catalog. Across every site there is no single
85
+ // catalog to filter by, and picking a site is how a reader gets one.
86
+ scoped || !hostId ? null : query(collection(firestore, 'hosts', hostId, 'forms'), orderBy('__name__'), limit(FORMS_MAX_PER_HOST + 1)), [
74
87
  firestore,
75
88
  hostId,
76
89
  scoped
77
90
  ], {
78
91
  idField: '$id'
79
92
  });
80
- /** More forms exist than the window shows; the filter has to say so. */ const formsTruncated = ((_ref = formDocs == null ? void 0 : formDocs.length) != null ? _ref : 0) > FORMS_MAX_PER_HOST;
93
+ /** More forms exist than the window shows; the filter has to say so. */ const formsTruncated = ((_ref1 = formDocs == null ? void 0 : formDocs.length) != null ? _ref1 : 0) > FORMS_MAX_PER_HOST;
81
94
  const forms = useMemo(()=>[
82
95
  ...formDocs != null ? formDocs : []
83
96
  ].slice(0, FORMS_MAX_PER_HOST).sort((left, right)=>{
@@ -119,15 +132,29 @@ export function SubmissionsCard({ hostId, formId }) {
119
132
  * that creates one and stamps `createdAt: serverTimestamp()` on every add,
120
133
  * the v1 API only ever reads and deletes, and `formSubmissions` is absent
121
134
  * from `IMPORTABLE_FIELDS`, so no restore path can make one without it.
122
- */ const { rows: submissions, hasMore: hasMoreSubmissions, page: submissionPage, setPage: setSubmissionPage, pageSize: submissionPageSize, setPageSize: setSubmissionPageSize } = usePagedCollection((pageLimit)=>query(collection(firestore, 'hosts', hostId, 'formSubmissions'), // Served by the `formId ASC, createdAt DESC` composite index in
135
+ */ const { rows: submissions, hasMore: hasMoreSubmissions, page: submissionPage, setPage: setSubmissionPage, pageSize: submissionPageSize, setPageSize: setSubmissionPageSize } = usePagedCollection((pageLimit)=>hostId ? query(collection(firestore, 'hosts', hostId, 'formSubmissions'), // Served by the `formId ASC, createdAt DESC` composite index in
123
136
  // `cloud/firebase-firestore.indexes.json`, which must be deployed
124
- // before this ships — without it Firestore refuses the query rather
125
- // than answering it slowly.
137
+ // before this ships — without it Firestore refuses the query
138
+ // rather than answering it slowly.
126
139
  ...activeForm ? [
127
140
  where('formId', '==', activeForm)
128
- ] : [], orderBy('createdAt', 'desc'), limit(pageLimit)), [
141
+ ] : [], orderBy('createdAt', 'desc'), limit(pageLimit)) : orgId ? query(/*
142
+ * EVERY SITE'S SUBMISSIONS IN ONE BOUNDED QUERY (AGL-3303), not
143
+ * a listener per site: the same page-plus-probe window, over
144
+ * the collection group, newest first.
145
+ *
146
+ * The `orgId` clause is not a filter this card could drop. The
147
+ * rules admit a group read only when it is narrowed to an org
148
+ * the reader spans, so an unfiltered group query is refused
149
+ * outright. Served by the `orgId ASC, createdAt DESC`
150
+ * COLLECTION_GROUP index. A row written before the submit route
151
+ * stamped `orgId` is in no organization's list until the scope
152
+ * backfill (`docs/SUBMISSION_SCOPE_BACKFILL.md`) stamps it; its
153
+ * own site's Inbox lists it either way.
154
+ */ collectionGroup(firestore, 'formSubmissions'), where('orgId', '==', orgId), orderBy('createdAt', 'desc'), limit(pageLimit)) : null, [
129
155
  firestore,
130
156
  hostId,
157
+ orgId,
131
158
  activeForm
132
159
  ], {
133
160
  idField: '$id'
@@ -137,22 +164,25 @@ export function SubmissionsCard({ hostId, formId }) {
137
164
  const [reader, setReader] = useState(null);
138
165
  const handleOpenReader = useCallback((submission)=>()=>{
139
166
  setReader(submission);
140
- if (!submission.read) {
141
- void updateDoc(doc(firestore, 'hosts', hostId, 'formSubmissions', submission.$id), {
167
+ const site = siteOf(submission);
168
+ if (!submission.read && site) {
169
+ void updateDoc(doc(firestore, 'hosts', site, 'formSubmissions', submission.$id), {
142
170
  read: true
143
171
  });
144
172
  }
145
173
  }, [
146
174
  firestore,
147
- hostId
175
+ siteOf
148
176
  ]);
149
177
  const handleToggleRead = useCallback((submission)=>()=>{
150
- void updateDoc(doc(firestore, 'hosts', hostId, 'formSubmissions', submission.$id), {
178
+ const site = siteOf(submission);
179
+ if (!site) return;
180
+ void updateDoc(doc(firestore, 'hosts', site, 'formSubmissions', submission.$id), {
151
181
  read: !submission.read
152
182
  });
153
183
  }, [
154
184
  firestore,
155
- hostId
185
+ siteOf
156
186
  ]);
157
187
  /*
158
188
  * A submission named in the URL opens in the reader on arrival (AGL-2622).
@@ -173,9 +203,12 @@ export function SubmissionsCard({ hostId, formId }) {
173
203
  * cleanup keyed on any dep would cancel the answer to the question the
174
204
  * URL just asked.
175
205
  */ const searchParams = useSearchParams();
176
- const seededSubmissionId = (_ref1 = searchParams == null ? void 0 : searchParams.get(INBOX_SUBMISSION_PARAM)) != null ? _ref1 : null;
206
+ const seededSubmissionId = (_ref2 = searchParams == null ? void 0 : searchParams.get(INBOX_SUBMISSION_PARAM)) != null ? _ref2 : null;
177
207
  const seededOpened = useRef(null);
178
208
  useEffect(()=>{
209
+ // A submission link names one site's Inbox; the org's has no site to read
210
+ // the id under.
211
+ if (!hostId) return;
179
212
  if (!seededSubmissionId || seededOpened.current === seededSubmissionId) return;
180
213
  seededOpened.current = seededSubmissionId;
181
214
  void getDoc(doc(firestore, 'hosts', hostId, 'formSubmissions', seededSubmissionId)).then((snapshot)=>{
@@ -199,6 +232,8 @@ export function SubmissionsCard({ hostId, formId }) {
199
232
  enqueueSnackbar
200
233
  ]);
201
234
  const handleDelete = useCallback((submission)=>async ()=>{
235
+ const site = siteOf(submission);
236
+ if (!site) return;
202
237
  const confirmed = await confirm({
203
238
  title: 'Delete this submission?',
204
239
  description: 'The submission is removed permanently.',
@@ -208,7 +243,7 @@ export function SubmissionsCard({ hostId, formId }) {
208
243
  }
209
244
  }).then(()=>true).catch(()=>false);
210
245
  if (!confirmed) return;
211
- await deleteDoc(doc(firestore, 'hosts', hostId, 'formSubmissions', submission.$id));
246
+ await deleteDoc(doc(firestore, 'hosts', site, 'formSubmissions', submission.$id));
212
247
  enqueueSnackbar('Submission deleted', {
213
248
  variant: 'success',
214
249
  persist: false
@@ -216,7 +251,7 @@ export function SubmissionsCard({ hostId, formId }) {
216
251
  }, [
217
252
  confirm,
218
253
  firestore,
219
- hostId,
254
+ siteOf,
220
255
  enqueueSnackbar
221
256
  ]);
222
257
  /*
@@ -335,6 +370,18 @@ export function SubmissionsCard({ hostId, formId }) {
335
370
  });
336
371
  }
337
372
  },
373
+ /*
374
+ Which site it was sent to, on the organization's Inbox only — under a
375
+ site every row is that site's, and a column saying so would be noise.
376
+ */ ...hostId == null ? [
377
+ {
378
+ field: 'hostId',
379
+ headerName: 'Site',
380
+ flex: 1,
381
+ minWidth: 140,
382
+ valueGetter: (_value, submission)=>orgSiteName(orgMount, submission.hostId)
383
+ }
384
+ ] : [],
338
385
  {
339
386
  field: 'message',
340
387
  headerName: 'Message',
@@ -390,13 +437,17 @@ export function SubmissionsCard({ hostId, formId }) {
390
437
  width: 72
391
438
  })
392
439
  ];
440
+ /** The site the open submission was sent to, which its reader acts as. */ const readerSite = reader ? siteOf(reader) : null;
393
441
  return /*#__PURE__*/ _jsxs(_Fragment, {
394
442
  children: [
395
443
  /*#__PURE__*/ _jsxs(CardDisplay, {
396
444
  header: scoped ? 'Submissions to this form' : 'Form Submissions',
397
- help: pluginDocsHelp('forms', {
445
+ help: pluginDocsHelp('forms', hostId == null ? {
446
+ anchor: '#every-sites-inbox-at-once',
447
+ excerpt: 'Every site’s form messages in one list, newest first, ' + 'each under the site it was sent to.'
448
+ } : {
398
449
  anchor: '#the-inbox',
399
- excerpt: 'Messages your forms collected, newest first, showing who sent ' + 'each one and where it was routed.'
450
+ excerpt: 'Messages your forms collected, newest first, showing who ' + 'sent each one and where it was routed.'
400
451
  }),
401
452
  contentGutterX: true,
402
453
  contentGutterY: true,
@@ -424,10 +475,18 @@ export function SubmissionsCard({ hostId, formId }) {
424
475
  }, form.$id))
425
476
  ]
426
477
  }) : null,
478
+ hostId == null ? /*#__PURE__*/ _jsx(Typography, {
479
+ variant: "body2",
480
+ color: "text.secondary",
481
+ sx: {
482
+ mb: submissions.length ? 2 : 1
483
+ },
484
+ children: 'Every site’s submissions, newest first. Choose a site to ' + 'narrow them by form.'
485
+ }) : null,
427
486
  submissions.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
428
487
  variant: "body2",
429
488
  color: "text.secondary",
430
- children: scoped ? 'No submissions carry this form’s id yet. Messages this ' + 'form’s design collected before it became a form entity are ' + 'in the Inbox, filed under the name they were sent with.' : activeForm ? 'No submissions for this form yet. Submissions sent before ' + 'the form was created stay under All forms.' : 'No form submissions yet. Add a Contact Form element to a ' + 'screen — visitor messages arrive here.'
489
+ children: scoped ? 'No submissions carry this form’s id yet. Messages this ' + 'form’s design collected before it became a form entity are ' + 'in the Inbox, filed under the name they were sent with.' : activeForm ? 'No submissions for this form yet. Submissions sent before ' + 'the form was created stay under All forms.' : hostId == null ? 'No form submissions on any site yet.' : 'No form submissions yet. Add a Contact Form element to a ' + 'screen — visitor messages arrive here.'
431
490
  }) : /*#__PURE__*/ _jsxs(_Fragment, {
432
491
  children: [
433
492
  /*#__PURE__*/ _jsx(ListTable, {
@@ -466,7 +525,7 @@ export function SubmissionsCard({ hostId, formId }) {
466
525
  fullWidth: true,
467
526
  children: [
468
527
  /*#__PURE__*/ _jsx(DialogTitle, {
469
- children: (_ref2 = reader == null ? void 0 : reader.formName) != null ? _ref2 : 'Form submission'
528
+ children: (_ref3 = reader == null ? void 0 : reader.formName) != null ? _ref3 : 'Form submission'
470
529
  }),
471
530
  /*#__PURE__*/ _jsxs(DialogContent, {
472
531
  children: [
@@ -474,7 +533,8 @@ export function SubmissionsCard({ hostId, formId }) {
474
533
  variant: "caption",
475
534
  color: "text.secondary",
476
535
  children: [
477
- `Received ${(_ref3 = 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 ? _ref3 : ''}` + ((reader == null ? void 0 : reader.screenId) ? ` · screen ${reader.screenId}` : ''),
536
+ `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.
537
+ (hostId == null && readerSite ? ` · ${orgSiteName(orgMount, readerSite)}` : '') + ((reader == null ? void 0 : reader.screenId) ? ` · screen ${reader.screenId}` : ''),
478
538
  (reader == null ? void 0 : reader.path) ? ` · ${String(reader.path)}` : ''
479
539
  ]
480
540
  }),
@@ -485,7 +545,7 @@ export function SubmissionsCard({ hostId, formId }) {
485
545
  }),
486
546
  /*#__PURE__*/ _jsx(Stack, {
487
547
  spacing: 1.5,
488
- children: Object.entries((_ref4 = reader == null ? void 0 : reader.fields) != null ? _ref4 : {}).map(([key, value])=>/*#__PURE__*/ _jsxs(Stack, {
548
+ children: Object.entries((_ref5 = reader == null ? void 0 : reader.fields) != null ? _ref5 : {}).map(([key, value])=>/*#__PURE__*/ _jsxs(Stack, {
489
549
  spacing: 0.25,
490
550
  children: [
491
551
  /*#__PURE__*/ _jsx(Typography, {
@@ -519,22 +579,22 @@ export function SubmissionsCard({ hostId, formId }) {
519
579
  variant: "outlined"
520
580
  }, chip.label))
521
581
  }),
522
- (reader == null ? void 0 : reader.$id) ? /*#__PURE__*/ _jsx(Box, {
582
+ (reader == null ? void 0 : reader.$id) && readerSite ? /*#__PURE__*/ _jsx(Box, {
523
583
  sx: {
524
584
  mt: 2
525
585
  },
526
586
  children: /*#__PURE__*/ _jsx(InboxRecordAttributionZone, {
527
- hostId: hostId,
587
+ hostId: readerSite,
528
588
  recordKind: "form",
529
589
  recordId: String(reader.$id)
530
590
  })
531
591
  }) : null,
532
- reader ? /*#__PURE__*/ _jsx(SubmissionReply, {
533
- hostId: hostId,
592
+ reader && readerSite ? /*#__PURE__*/ _jsx(SubmissionReply, {
593
+ hostId: readerSite,
534
594
  submission: reader
535
595
  }) : null,
536
- reader ? /*#__PURE__*/ _jsx(SubmissionListAssignment, {
537
- hostId: hostId,
596
+ reader && readerSite ? /*#__PURE__*/ _jsx(SubmissionListAssignment, {
597
+ hostId: readerSite,
538
598
  submission: reader
539
599
  }) : null
540
600
  ]
@@ -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 { FORMS_MAX_PER_HOST, INBOX_SUBMISSION_PARAM, pluginDocsHelp } from '@aglyn/aglyn'\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 { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.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 { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n usePagedCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Avatar,\n Box,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Divider,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n deleteDoc,\n doc,\n getDoc,\n limit,\n orderBy,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\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 SubmissionListAssignment from './submission-list-assignment.component'\nimport SubmissionReply from './submission-reply.component'\nimport { useRecordRouteContext } from './use-record-route-context'\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 hostId: string\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({ hostId, formId }: SubmissionsCardProps) {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n /** Scoped to one form: the subject is fixed and nothing may widen it. */\n const scoped = Boolean(formId)\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 scoped\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 /*\n * `null` is \"All forms\"; a form id narrows to one form's submissions.\n *\n * A PRIMITIVE, deliberately: `usePagedCollection` reopens its listener when\n * a dep changes, and an object identity would tear down and reopen on every\n * render. It also resets the reader to page 1, which is what switching\n * subjects should do.\n */\n const [formFilter, setFormFilter] = useState<string | null>(null)\n /**\n * The form the query is actually narrowed to.\n *\n * The scope wins over the picker rather than seeding it. A scoped card\n * renders no picker, so a `formFilter` that could outrank `formId` would be\n * a filter with no control — reachable only by a state change nothing on\n * screen can cause, and unclearable if one ever could.\n */\n const activeForm = formId ?? formFilter\n\n /*\n * The inbox WALKS its submissions instead of sampling them (AGL-2501,\n * AGL-2292).\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: `apps/tenant/app/api/forms/submit/route.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 const {\n rows: submissions,\n hasMore: hasMoreSubmissions,\n page: submissionPage,\n setPage: setSubmissionPage,\n pageSize: submissionPageSize,\n setPageSize: setSubmissionPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n query(\n collection(firestore, 'hosts', hostId, 'formSubmissions'),\n // Served by the `formId ASC, createdAt DESC` composite index in\n // `cloud/firebase-firestore.indexes.json`, which must be deployed\n // before this ships — without it Firestore refuses the query rather\n // than answering it slowly.\n ...(activeForm ? [where('formId', '==', activeForm)] : []),\n orderBy('createdAt', 'desc'),\n limit(pageLimit),\n ),\n [firestore, hostId, activeForm],\n { idField: '$id' },\n )\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 if (!submission.read) {\n void updateDoc(\n doc(firestore, 'hosts', hostId, 'formSubmissions', submission.$id),\n { read: true },\n )\n }\n },\n [firestore, hostId],\n )\n\n const handleToggleRead = useCallback(\n (submission: any) => () => {\n void updateDoc(\n doc(firestore, 'hosts', hostId, 'formSubmissions', submission.$id),\n { read: !submission.read },\n )\n },\n [firestore, hostId],\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 if (!seededSubmissionId || seededOpened.current === seededSubmissionId) return\n seededOpened.current = seededSubmissionId\n void getDoc(doc(firestore, 'hosts', hostId, 'formSubmissions', seededSubmissionId))\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 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(\n doc(firestore, 'hosts', hostId, 'formSubmissions', submission.$id),\n )\n enqueueSnackbar('Submission deleted', {\n variant: 'success',\n persist: false,\n })\n },\n [confirm, firestore, hostId, enqueueSnackbar],\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 field: 'message',\n headerName: 'Message',\n flex: 2,\n minWidth: 260,\n valueGetter: (_value, submission) =>\n Object.entries(submission.fields ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join(' · '),\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 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 return (\n <>\n <CardDisplay\n header={scoped ? 'Submissions to this form' : 'Form Submissions'}\n help={pluginDocsHelp('forms', {\n anchor: '#the-inbox',\n excerpt:\n 'Messages your forms collected, newest first, showing who sent ' +\n 'each one and where it was routed.',\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. This narrows\n * it on request; it does not turn the page into a per-form view.\n *\n * Rendered only when the site HAS forms, so a site that has not\n * adopted any sees the page exactly as it was.\n */}\n {/*\n * Withheld when the card is scoped: the subject is settled by the\n * surface this is rendered on, and a picker offering to widen it\n * would be a control that navigates away from the page it is on.\n */}\n {!scoped && forms.length > 0 ? (\n <TextField\n select\n size=\"small\"\n label={'Form'}\n value={formFilter ?? ''}\n onChange={(event) => setFormFilter(event.target.value || null)}\n helperText={\n formsTruncated\n ? `Showing the first ${FORMS_MAX_PER_HOST.toLocaleString()} ` +\n 'forms. Narrow by form is incomplete; All forms still ' +\n 'covers every submission.'\n : undefined\n }\n sx={{ mb: 2, minWidth: 240 }}\n >\n <MenuItem value=\"\">{'All forms'}</MenuItem>\n {forms.map((form: any) => (\n <MenuItem key={form.$id} value={form.$id}>\n {form.displayName || form.$id}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {submissions.length === 0 ? (\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 : activeForm\n ? 'No submissions for this form yet. Submissions sent before ' +\n 'the form was created stay under All forms.'\n : 'No form submissions yet. Add a Contact Form element to a ' +\n 'screen — visitor messages arrive here.'}\n </Typography>\n ) : (\n <>\n <ListTable\n aria-label={scoped ? 'Submissions to this form' : 'Form submissions'}\n rows={submissions}\n columns={submissionColumns}\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 />\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 (reader?.screenId ? ` · screen ${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 ? (\n <Box sx={{ mt: 2 }}>\n <InboxRecordAttributionZone\n hostId={hostId}\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 ? (\n <SubmissionReply hostId={hostId} 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 ? (\n <SubmissionListAssignment hostId={hostId} 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","INBOX_SUBMISSION_PARAM","pluginDocsHelp","InboxRecordAttributionZone","pluginRecordByEmailHref","pluginRecordListHref","mdiAccountArrowRight","mdiDeleteOutline","mdiEmailOpenOutline","mdiEmailOutline","CardDisplay","MdiIcon","useConfirmationContext","ListPagination","ListRowActions","ListTable","listActionsColumn","TABLE_ROW_HEIGHT","useSnackbar","useFirestore","useFirestoreCollection","usePagedCollection","Avatar","Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Divider","MenuItem","Stack","TextField","Tooltip","Typography","collection","deleteDoc","doc","getDoc","limit","orderBy","query","updateDoc","where","useSearchParams","useCallback","useEffect","useMemo","useRef","useState","relativeTime","routingChips","senderHue","submissionSender","SubmissionListAssignment","SubmissionReply","useRecordRouteContext","SubmissionsCard","hostId","formId","reader","firestore","enqueueSnackbar","confirm","scoped","Boolean","routeContext","data","formDocs","idField","formsTruncated","length","forms","slice","sort","left","right","String","displayName","$id","localeCompare","formFilter","setFormFilter","activeForm","rows","submissions","hasMore","hasMoreSubmissions","page","submissionPage","setPage","setSubmissionPage","pageSize","submissionPageSize","setPageSize","setSubmissionPageSize","pageLimit","setReader","handleOpenReader","submission","read","handleToggleRead","searchParams","seededSubmissionId","get","seededOpened","current","then","snapshot","id","exists","variant","persist","catch","undefined","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","submissionActions","senderEmail","fields","email","contactsHref","key","label","icon","path","size","href","disabled","disabledReason","onClick","destructive","submissionColumns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","row","sender","hue","direction","spacing","sx","alignItems","aria-label","width","height","borderRadius","bgcolor","flexShrink","fontSize","initials","noWrap","formName","Object","entries","map","value","join","createdAt","toDate","getTime","toLocaleString","span","items","header","help","anchor","excerpt","contentGutterX","contentGutterY","contentBordered","select","onChange","event","target","helperText","mb","form","columns","rowHeight","onOpen","_id","getRowClassName","fontWeight","hideFooter","rowCount","onPageChange","onPageSizeChange","open","onClose","maxWidth","fullWidth","screenId","my","whiteSpace","wordBreak","mt","flexWrap","rowGap","routing","chip","recordKind","recordId"],"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 INBOX_SUBMISSION_PARAM,\n pluginDocsHelp,\n type ConsolePluginOrgMount,\n} from '@aglyn/aglyn'\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 { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.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 { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n usePagedCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Avatar,\n Box,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Divider,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n collectionGroup,\n deleteDoc,\n doc,\n getDoc,\n limit,\n orderBy,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\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 { 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 * 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 { 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 /*\n * `null` is \"All forms\"; a form id narrows to one form's submissions.\n *\n * A PRIMITIVE, deliberately: `usePagedCollection` reopens its listener when\n * a dep changes, and an object identity would tear down and reopen on every\n * render. It also resets the reader to page 1, which is what switching\n * subjects should do.\n */\n const [formFilter, setFormFilter] = useState<string | null>(null)\n /**\n * The form the query is actually narrowed to.\n *\n * The scope wins over the picker rather than seeding it. A scoped card\n * renders no picker, so a `formFilter` that could outrank `formId` would be\n * a filter with no control — reachable only by a state change nothing on\n * screen can cause, and unclearable if one ever could.\n */\n const activeForm = formId ?? formFilter\n\n /*\n * The inbox WALKS its submissions instead of sampling them (AGL-2501,\n * AGL-2292).\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: `apps/tenant/app/api/forms/submit/route.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 const {\n rows: submissions,\n hasMore: hasMoreSubmissions,\n page: submissionPage,\n setPage: setSubmissionPage,\n pageSize: submissionPageSize,\n setPageSize: setSubmissionPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n hostId\n ? query(\n collection(firestore, 'hosts', hostId, 'formSubmissions'),\n // Served by the `formId ASC, createdAt DESC` composite index in\n // `cloud/firebase-firestore.indexes.json`, which must be deployed\n // before this ships — without it Firestore refuses the query\n // rather than answering it slowly.\n ...(activeForm ? [where('formId', '==', activeForm)] : []),\n orderBy('createdAt', 'desc'),\n limit(pageLimit),\n )\n : orgId\n ? query(\n /*\n * EVERY SITE'S SUBMISSIONS IN ONE BOUNDED QUERY (AGL-3303), not\n * a listener per site: the same page-plus-probe window, over\n * the collection group, newest first.\n *\n * The `orgId` clause is not a filter this card could drop. The\n * rules admit a group read only when it is narrowed to an org\n * the reader spans, so an unfiltered group query is refused\n * outright. Served by the `orgId ASC, createdAt DESC`\n * COLLECTION_GROUP index. A row written before the submit route\n * stamped `orgId` is in no organization's list until the scope\n * backfill (`docs/SUBMISSION_SCOPE_BACKFILL.md`) stamps it; its\n * own site's Inbox lists it either way.\n */\n collectionGroup(firestore, 'formSubmissions'),\n where('orgId', '==', orgId),\n orderBy('createdAt', 'desc'),\n limit(pageLimit),\n )\n : null,\n [firestore, hostId, orgId, activeForm],\n { idField: '$id' },\n )\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 if (!submission.read && site) {\n void updateDoc(\n doc(firestore, 'hosts', site, 'formSubmissions', submission.$id),\n { read: true },\n )\n }\n },\n [firestore, siteOf],\n )\n\n const handleToggleRead = useCallback(\n (submission: any) => () => {\n const site = siteOf(submission)\n if (!site) return\n void updateDoc(\n doc(firestore, 'hosts', site, 'formSubmissions', submission.$id),\n { read: !submission.read },\n )\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 void getDoc(doc(firestore, 'hosts', hostId, 'formSubmissions', seededSubmissionId))\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 if (!site) 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(\n doc(firestore, 'hosts', site, 'formSubmissions', submission.$id),\n )\n enqueueSnackbar('Submission deleted', {\n variant: 'success',\n persist: false,\n })\n },\n [confirm, firestore, siteOf, enqueueSnackbar],\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 valueGetter: (_value: unknown, submission: 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) =>\n Object.entries(submission.fields ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join(' · '),\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 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. This narrows\n * it on request; it does not turn the page into a per-form view.\n *\n * Rendered only when the site HAS forms, so a site that has not\n * adopted any sees the page exactly as it was.\n */}\n {/*\n * Withheld when the card is scoped: the subject is settled by the\n * surface this is rendered on, and a picker offering to widen it\n * would be a control that navigates away from the page it is on.\n */}\n {!scoped && forms.length > 0 ? (\n <TextField\n select\n size=\"small\"\n label={'Form'}\n value={formFilter ?? ''}\n onChange={(event) => setFormFilter(event.target.value || null)}\n helperText={\n formsTruncated\n ? `Showing the first ${FORMS_MAX_PER_HOST.toLocaleString()} ` +\n 'forms. Narrow by form is incomplete; All forms still ' +\n 'covers every submission.'\n : undefined\n }\n sx={{ mb: 2, minWidth: 240 }}\n >\n <MenuItem value=\"\">{'All forms'}</MenuItem>\n {forms.map((form: any) => (\n <MenuItem key={form.$id} value={form.$id}>\n {form.displayName || form.$id}\n </MenuItem>\n ))}\n </TextField>\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 ? (\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 : activeForm\n ? 'No submissions for this form yet. Submissions sent before ' +\n 'the form was created stay under All forms.'\n : hostId == null\n ? 'No form submissions on any site yet.'\n : 'No form submissions yet. Add a Contact Form element to a ' +\n 'screen — visitor messages arrive here.'}\n </Typography>\n ) : (\n <>\n <ListTable\n aria-label={scoped ? 'Submissions to this form' : 'Form submissions'}\n rows={submissions}\n columns={submissionColumns}\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 />\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 ? ` · screen ${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","INBOX_SUBMISSION_PARAM","pluginDocsHelp","InboxRecordAttributionZone","pluginRecordByEmailHref","pluginRecordListHref","mdiAccountArrowRight","mdiDeleteOutline","mdiEmailOpenOutline","mdiEmailOutline","CardDisplay","MdiIcon","useConfirmationContext","ListPagination","ListRowActions","ListTable","listActionsColumn","TABLE_ROW_HEIGHT","useSnackbar","useFirestore","useFirestoreCollection","usePagedCollection","Avatar","Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Divider","MenuItem","Stack","TextField","Tooltip","Typography","collection","collectionGroup","deleteDoc","doc","getDoc","limit","orderBy","query","updateDoc","where","useSearchParams","useCallback","useEffect","useMemo","useRef","useState","relativeTime","routingChips","senderHue","submissionSender","orgSiteName","SubmissionListAssignment","SubmissionReply","useRecordRouteContext","SubmissionsCard","hostId","formId","orgMount","reader","firestore","enqueueSnackbar","confirm","scoped","Boolean","orgId","siteOf","submission","routeContext","data","formDocs","idField","formsTruncated","length","forms","slice","sort","left","right","String","displayName","$id","localeCompare","formFilter","setFormFilter","activeForm","rows","submissions","hasMore","hasMoreSubmissions","page","submissionPage","setPage","setSubmissionPage","pageSize","submissionPageSize","setPageSize","setSubmissionPageSize","pageLimit","setReader","handleOpenReader","site","read","handleToggleRead","searchParams","seededSubmissionId","get","seededOpened","current","then","snapshot","id","exists","variant","persist","catch","undefined","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","submissionActions","senderEmail","fields","email","contactsHref","key","label","icon","path","size","href","disabled","disabledReason","onClick","destructive","submissionColumns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","row","sender","hue","direction","spacing","sx","alignItems","aria-label","width","height","borderRadius","bgcolor","flexShrink","fontSize","initials","noWrap","formName","Object","entries","map","value","join","createdAt","toDate","getTime","toLocaleString","span","items","readerSite","header","help","anchor","excerpt","contentGutterX","contentGutterY","contentBordered","select","onChange","event","target","helperText","mb","form","columns","rowHeight","onOpen","_id","getRowClassName","fontWeight","hideFooter","rowCount","onPageChange","onPageSizeChange","open","onClose","maxWidth","fullWidth","screenId","my","whiteSpace","wordBreak","mt","flexWrap","rowGap","routing","chip","recordKind","recordId"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { ConsolePluginOrgMount } from '@aglyn/aglyn';
2
+ /**
3
+ * The site the organization's Inbox is narrowed to (AGL-3303).
4
+ *
5
+ * `hostId` is `null` for every site at once — the default — or the one site
6
+ * the reader picked, whose own view the page then draws: its submissions
7
+ * with their form filter, its members beside its leads, its campaigns and
8
+ * its ceiling notices. An organization with ONE site has nothing to choose
9
+ * between, so that site is the pick and no picker is offered.
10
+ */
11
+ export interface InboxSitePick {
12
+ hostId: string | null;
13
+ /**
14
+ * False until the remembered pick has been read and the org's sites have
15
+ * settled. A section drawn before then would open the every-site reads for
16
+ * a page about to switch to one site's, and bill both.
17
+ */
18
+ ready: boolean;
19
+ /** The sites to choose between, by name — empty when there is one or none. */
20
+ options: ReadonlyArray<{
21
+ value: string;
22
+ label: string;
23
+ }>;
24
+ setHostId: (hostId: string | null) => void;
25
+ }
26
+ /**
27
+ * The session's memory of the pick, per org. Session storage, as the CRM's
28
+ * create-site pick is kept: the section rail is a set of routes, so the page
29
+ * mounts again on every section, and a filter that reset on each one would
30
+ * read as the rail having thrown the reader's choice away.
31
+ */
32
+ export declare const inboxSitePickKey: (orgId: string) => string;
33
+ /**
34
+ * The org Inbox's site filter. Handed no mount — under a site — it answers
35
+ * no pick and is never ready, and the page reads its own `hostId` instead.
36
+ */
37
+ export declare function useInboxSitePick(mount: ConsolePluginOrgMount | undefined): InboxSitePick;
38
+ export default useInboxSitePick;