@aglyn/plugins-inbox 1.0.0-beta.232 → 1.0.0-beta.234
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 +11 -11
- package/src/lib/components/contacts-card.component.js +34 -8
- package/src/lib/components/contacts-card.component.js.map +1 -1
- package/src/lib/components/submissions-card.component.js +40 -7
- package/src/lib/components/submissions-card.component.js.map +1 -1
- package/src/lib/constants/list-queries.js +68 -15
- package/src/lib/constants/list-queries.js.map +1 -1
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.234",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,16 +25,16 @@
|
|
|
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/shared-util-tools": "1.0.0-beta.
|
|
36
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.234",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.234",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.234",
|
|
31
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.234",
|
|
32
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.234",
|
|
33
|
+
"@aglyn/shared-util-email": "1.0.0-beta.234",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.234",
|
|
35
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.234",
|
|
36
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.234",
|
|
37
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.234",
|
|
38
38
|
"@swc/helpers": "0.5.23"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
@@ -39,6 +39,7 @@ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
|
39
39
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
40
40
|
import { useFirestore, useOrgDataScope, useScopeTokens, useUser } from "@aglyn/tenant-feature-instance";
|
|
41
41
|
import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
42
|
+
import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
|
|
42
43
|
import { Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Stack, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material";
|
|
43
44
|
import { collection } from "firebase/firestore";
|
|
44
45
|
import { useCallback, useMemo, useState } from "react";
|
|
@@ -102,6 +103,12 @@ const NO_SELECT_FIELDS = [];
|
|
|
102
103
|
}
|
|
103
104
|
});
|
|
104
105
|
/*
|
|
106
|
+
* EVERY HEADER SORTS (AGL-3680). Email and Joined/Captured order the QUERY
|
|
107
|
+
* (`contactSorts`): both are on every member and lead. Source and Site are
|
|
108
|
+
* sets on a lead, so they sort the page on screen and say so. Null is the
|
|
109
|
+
* list's own order, newest first; a switch of list starts it again.
|
|
110
|
+
*/ const [askedSort, setAskedSort] = useState(null);
|
|
111
|
+
/*
|
|
105
112
|
* A switch of list starts it clean: the two lists filter by different
|
|
106
113
|
* fields, and a clause carried across would be one the other list cannot
|
|
107
114
|
* ask — refused on arrival, for a question the reader asked of the list
|
|
@@ -110,11 +117,13 @@ const NO_SELECT_FIELDS = [];
|
|
|
110
117
|
if (!next || next === view) return;
|
|
111
118
|
gridFilter.setClauses([]);
|
|
112
119
|
setSearchWords([]);
|
|
120
|
+
setAskedSort(null);
|
|
113
121
|
setChosen(next);
|
|
114
122
|
};
|
|
115
123
|
const request = {
|
|
116
124
|
clauses: gridFilter.clauses,
|
|
117
|
-
search: gridFilter.searchWords
|
|
125
|
+
search: gridFilter.searchWords,
|
|
126
|
+
sort: askedSort
|
|
118
127
|
};
|
|
119
128
|
/*==========================================
|
|
120
129
|
* SITE MEMBERS — `hosts/{hostId}/siteMembers`, newest first
|
|
@@ -229,6 +238,22 @@ const NO_SELECT_FIELDS = [];
|
|
|
229
238
|
addressSearched,
|
|
230
239
|
shown.plan.notices
|
|
231
240
|
]);
|
|
241
|
+
const contactPageSorts = useMemo(()=>({
|
|
242
|
+
sources: (lead)=>leadSources(lead).map(leadSourceLabel).join(', '),
|
|
243
|
+
capturedByHostIds: (contact)=>orgSiteNames(orgMount, contact.capturedByHostIds)
|
|
244
|
+
}), [
|
|
245
|
+
orgMount
|
|
246
|
+
]);
|
|
247
|
+
const columnSort = useListColumnSort({
|
|
248
|
+
sorts: declaration.sorts,
|
|
249
|
+
defaultSort: declaration.sorts[0],
|
|
250
|
+
sort: askedSort,
|
|
251
|
+
onSortChange: setAskedSort,
|
|
252
|
+
orderBy: shown.plan.orderBy,
|
|
253
|
+
rows: shown.rows,
|
|
254
|
+
pageSorts: view === 'leads' ? contactPageSorts : undefined,
|
|
255
|
+
headers
|
|
256
|
+
});
|
|
232
257
|
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
233
258
|
/*
|
|
234
259
|
* REMOVED BY THE ROUTE THAT OWNS MEMBER ACCOUNTS (AGL-3308), not by a
|
|
@@ -387,7 +412,6 @@ const NO_SELECT_FIELDS = [];
|
|
|
387
412
|
field: 'sources',
|
|
388
413
|
headerName: 'Source',
|
|
389
414
|
width: 200,
|
|
390
|
-
sortable: false,
|
|
391
415
|
valueGetter: (_value, lead)=>leadSources(lead).map(leadSourceLabel).join(', '),
|
|
392
416
|
renderCell: ({ row: lead })=>/*#__PURE__*/ _jsx(Stack, {
|
|
393
417
|
direction: "row",
|
|
@@ -416,7 +440,6 @@ const NO_SELECT_FIELDS = [];
|
|
|
416
440
|
headerName: 'Site',
|
|
417
441
|
flex: 1,
|
|
418
442
|
minWidth: 160,
|
|
419
|
-
sortable: false,
|
|
420
443
|
renderCell: ({ row: contact })=>orgSiteNames(orgMount, contact.capturedByHostIds)
|
|
421
444
|
}
|
|
422
445
|
] : [],
|
|
@@ -504,22 +527,25 @@ const NO_SELECT_FIELDS = [];
|
|
|
504
527
|
}),
|
|
505
528
|
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
506
529
|
refused: refusals,
|
|
507
|
-
notices:
|
|
530
|
+
notices: [
|
|
531
|
+
...notices,
|
|
532
|
+
...columnSort.notices
|
|
533
|
+
]
|
|
508
534
|
}),
|
|
509
535
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
510
536
|
"aria-label": view === 'members' ? 'Site members' : 'Leads',
|
|
511
|
-
rows:
|
|
537
|
+
rows: columnSort.rows,
|
|
538
|
+
columnSort: columnSort,
|
|
512
539
|
columns: listFilterGridColumns(columns, declaration.fields, options, headers),
|
|
513
540
|
rowHeight: TABLE_ROW_HEIGHT,
|
|
514
541
|
// Paged by the footer below, so the grid must not also slice.
|
|
515
542
|
hideFooter: true,
|
|
516
|
-
// The panel and the
|
|
517
|
-
//
|
|
543
|
+
// The panel, the search and the headers go to the query
|
|
544
|
+
// (Source and Site sort the page, and say so).
|
|
518
545
|
filterMode: "server",
|
|
519
546
|
filterModel: gridFilter.filterModel,
|
|
520
547
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
521
548
|
quickFilter: true,
|
|
522
|
-
disableColumnSorting: true,
|
|
523
549
|
noRowsLabel: view === 'members' ? 'No members match these filters' : 'No leads match these filters'
|
|
524
550
|
}, view),
|
|
525
551
|
/*#__PURE__*/ _jsx(ListPagination, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/contacts-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 { pluginDocsHelp, type ConsolePluginOrgMount } 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, not the plugin barrel: the barrel\n// carries the plugin registration, and a link needs only the address grammar.\nimport { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n mdiAccountArrowRight,\n mdiAccountRemoveOutline,\n mdiBullhornOutline,\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 { 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 { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n useFirestore,\n useOrgDataScope,\n useScopeTokens,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n LEAD_FILTER_HEADERS,\n LEAD_LIST_QUERY,\n LEAD_SOURCE_OPTIONS,\n ORG_LEAD_LIST_QUERY,\n SITE_MEMBER_FILTER_HEADERS,\n SITE_MEMBER_LIST_QUERY,\n leadListBase,\n LEAD_ADDRESS_SEARCH_COLUMN,\n LEAD_ADDRESS_SEARCH_NOTICE,\n leadListQueryFor,\n leadSourceLabel,\n} from '../constants/list-queries'\nimport { orgSiteNames } from './inbox-org-sites'\nimport { useRecordRouteContext } from './use-record-route-context'\n\n/** Which of the two collections the card lists. */\nexport type ContactsView = 'members' | 'leads'\n\n/** The organization's Inbox's Source and Site are picked, so the panel shows selects. */\nconst ORG_LEAD_SELECT_FIELDS = ['sources', 'capturedByHostIds']\nconst NO_SELECT_FIELDS: readonly string[] = []\n\n/**\n * Every surface a lead was captured by: `sources`, which every capture\n * `arrayUnion`s, or the single `source` a lead written before it carries.\n */\nconst leadSources = (lead: any): string[] =>\n Array.isArray(lead?.sources)\n ? lead.sources.filter((source: unknown): source is string => typeof source === 'string')\n : typeof lead?.source === 'string' && lead.source\n ? [lead.source]\n : []\n\n/**\n * The Members & leads section of the Inbox (AGL-109): the people a site\n * collected — its members, who signed up to it, and its leads, whom a form,\n * a booking or a teammate captured.\n *\n * ## Two lists, one table (AGL-3321)\n *\n * Members and leads are two collections, and the table shows one at a time,\n * chosen by the toggle above it. Each is its own Firestore query, every\n * filter and search word on it, paged by the server — so a page is a page of\n * the answer, and a site with thousands of either reaches all of them.\n *\n * It used to read the two hundred newest of each and interleave them, hiding\n * a lead whose address matched a member. That dedupe is only correct while\n * both reads are whole, which is exactly what a paged query is not, so it is\n * gone: a person who left their address and later signed up is a lead in one\n * list and a member in the other, which is what they are in each collection.\n *\n * On the organization's Inbox (`hostId: null`, AGL-3303) it lists the\n * organization's leads with the sites each was captured by. A member signs up\n * to ONE site and lives under it, so Members waits for a site to be picked,\n * which is the page handing this card that site.\n */\nexport function ContactsCard({\n hostId,\n orgMount,\n}: {\n hostId: string | null\n /** The organization and its sites — present exactly when `hostId` is `null`. */\n orgMount?: ConsolePluginOrgMount\n}) {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // Where a lead is WORKED (AGL-2608). This card lists leads; the CRM's\n // Leads section gives each one a status, an owner and a conversion.\n const routeContext = useRecordRouteContext()\n const leadHref = (contact: { $id?: unknown }): string | null =>\n routeContext\n ? pluginRecordHref('lead', routeContext, String(contact.$id))\n : null\n\n /*\n * The list shown. Members exist only under a site, so the organization's\n * Inbox shows leads whatever was chosen, and the toggle says why.\n */\n const [chosen, setChosen] = useState<ContactsView>('members')\n const view: ContactsView = hostId ? chosen : 'leads'\n const [searchWords, setSearchWords] = useState<string[]>([])\n const gridFilter = useListGridFilter({\n selectFields: view === 'leads' && hostId == null ? ORG_LEAD_SELECT_FIELDS : NO_SELECT_FIELDS,\n search: { words: searchWords, onChange: setSearchWords },\n })\n /*\n * A switch of list starts it clean: the two lists filter by different\n * fields, and a clause carried across would be one the other list cannot\n * ask — refused on arrival, for a question the reader asked of the list\n * they just left.\n */\n const switchView = (next: ContactsView | null) => {\n if (!next || next === view) return\n gridFilter.setClauses([])\n setSearchWords([])\n setChosen(next)\n }\n const request = { clauses: gridFilter.clauses, search: gridFilter.searchWords }\n\n /*==========================================\n * SITE MEMBERS — `hosts/{hostId}/siteMembers`, newest first\n * (`SITE_MEMBER_LIST_QUERY`). `createdAt` is safe to order on:\n * `membership-register.ts` is the only writer that CREATES a site member\n * and stamps it inside its transaction, and the collection is not in\n * `IMPORTABLE_FIELDS`. No query opens while the other list is shown.\n *=========================================*/\n const members = useListQuery<any>({\n collection:\n hostId && view === 'members'\n ? collection(firestore, 'hosts', hostId, 'siteMembers')\n : null,\n declaration: SITE_MEMBER_LIST_QUERY,\n request: view === 'members' ? request : { clauses: [] },\n deps: [firestore, hostId, view],\n idField: '$id',\n })\n\n /*==========================================\n * LEADS — `orgs/{orgId}/leads`, newest first (`LEAD_LIST_QUERY`). The\n * silo is the org's (AGL-3275), narrowed to this site by `visibleTo`;\n * reading `hosts/{hostId}/leads` showed a site its pre-migration rows and\n * nothing captured since. `addHostLead`, every lead door's one writer,\n * stamps `createdAt` on every create.\n *\n * Every site's, on the organization's Inbox: an org-wide member reads the\n * collection unscoped — the rules short-circuit on their reach, as for the\n * CRM's own org list — so a clause there would only narrow what they may\n * already read.\n *=========================================*/\n const { orgId } = useOrgDataScope({\n hostId: hostId ?? undefined,\n orgId: hostId == null ? orgMount?.orgId : undefined,\n })\n /*\n * Whether the search may fold into the scope clause (`scopedSearch`): a\n * query without `visibleTo` is one the rules prove only for an ORG-WIDE\n * member. Anyone else keeps the clause, and the plan refuses their search\n * by name rather than sending a query the rules deny. Until the reach has\n * answered, the answer is no. Asked only while Leads is shown: the member\n * document is a read, and Members has no scope to fold into.\n */\n const reach = useScopeTokens(hostId && orgId && view === 'leads' ? orgId : undefined)\n const foldsScope = hostId == null || (reach.loaded && reach.orgWide)\n const leadDeclaration = hostId ? LEAD_LIST_QUERY : ORG_LEAD_LIST_QUERY\n const leadBase = useMemo(\n () => leadListBase(hostId ? scopeTokensForHost(hostId) : null),\n [hostId],\n )\n const leadQuery = useMemo(\n () =>\n leadListQueryFor(\n leadDeclaration,\n foldsScope,\n view === 'leads' ? { ...request, base: leadBase } : { clauses: [] },\n ),\n // The request is data; its JSON is its identity.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [leadDeclaration, foldsScope, view, leadBase, JSON.stringify(request)],\n )\n const leads = useListQuery<any>({\n collection:\n orgId && view === 'leads' ? collection(firestore, 'orgs', orgId, 'leads') : null,\n declaration: leadQuery.declaration,\n request: leadQuery.request,\n deps: [firestore, orgId, hostId, view],\n idField: '$id',\n })\n\n const shown = view === 'members' ? members : leads\n const declaration = view === 'members' ? SITE_MEMBER_LIST_QUERY : leadDeclaration\n const headers = view === 'members' ? SITE_MEMBER_FILTER_HEADERS : LEAD_FILTER_HEADERS\n const options = useMemo<Readonly<Record<string, readonly ListFilterOption[]>>>(\n () =>\n view === 'leads' && hostId == null\n ? {\n sources: LEAD_SOURCE_OPTIONS,\n capturedByHostIds: (orgMount?.hosts ?? []).map((host) => ({\n value: host.id,\n label: host.name || host.id,\n })),\n }\n : {},\n [view, hostId, orgMount],\n )\n /*\n * A site collaborator's search rides the query as an address-prefix clause\n * (`leadListQueryFor`); a refusal of it is the search's, and when it is\n * served the list says what it matched and how it is now ordered.\n */\n const refusals = useMemo(\n () =>\n listQueryRefusals(\n shown.plan.refused.map((entry) =>\n entry.clause !== 'search' && entry.clause.field === LEAD_ADDRESS_SEARCH_COLUMN\n ? { ...entry, clause: 'search' as const }\n : entry,\n ),\n { fields: declaration.fields, headers, options },\n ),\n [shown.plan.refused, declaration, headers, options],\n )\n const addressSearched =\n view === 'leads' &&\n leadQuery.addressSearch &&\n leads.plan.served.some((clause) => clause.field === LEAD_ADDRESS_SEARCH_COLUMN)\n const notices = useMemo(\n () => (addressSearched ? [...shown.plan.notices, LEAD_ADDRESS_SEARCH_NOTICE] : shown.plan.notices),\n [addressSearched, shown.plan.notices],\n )\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n\n /*\n * REMOVED BY THE ROUTE THAT OWNS MEMBER ACCOUNTS (AGL-3308), not by a\n * client delete. A member's password hash lives in a document no client can\n * reach, so deleting the profile from here would leave it behind: the route\n * deletes both, and the rules refuse a client delete of any member that\n * has one.\n */\n const handleDeleteMember = useCallback(\n (member: any) => async () => {\n // Members are listed only under a site, so there is one to delete from.\n if (!hostId) return\n const confirmed = await confirm({\n title: 'Remove this member?',\n description: `\"${member.email}\" can no longer sign in to your site.`,\n confirmationText: 'Remove',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n const response = await authorizedFetch(user, '/api/membership/admin-remove', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, memberId: member.$id }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(payload?.error ?? 'The member was not removed', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar('Member removed', { variant: 'success', persist: false })\n } catch {\n enqueueSnackbar('The member was not removed', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n },\n [confirm, user, hostId, enqueueSnackbar],\n )\n\n /*\n * WHERE A LEAD CAME FROM, on request.\n *\n * A lead is a table row with no page of its own, and the attribution is one\n * keyed document read — cheap on its own, and the page size times cheap in\n * a column. So it is an overflow action that opens a dialog: the reader who\n * wants the answer pays for it, and the reader who came to scan the list\n * does not.\n */\n const [leadOrigin, setLeadOrigin] = useState<any | null>(null)\n /*\n * The site a lead's attribution is asked of. Under a site it is that site;\n * across every site a lead is one org row that several sites may have met,\n * and the credit is filed by the site whose capture made it — the first in\n * `capturedByHostIds`, the site the CRM's own lead page asks as well.\n */\n const originSite = (contact: any): string | null => {\n if (hostId) return hostId\n const first = Array.isArray(contact?.capturedByHostIds)\n ? contact.capturedByHostIds[0]\n : null\n return typeof first === 'string' && first ? first : null\n }\n const leadOriginSite = leadOrigin ? originSite(leadOrigin) : null\n\n const memberActions = (member: any): RowActionsMenuItem[] => [\n {\n key: 'remove',\n label: 'Remove member',\n icon: <MdiIcon path={mdiAccountRemoveOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDeleteMember(member)(),\n },\n ]\n const leadActions = (lead: any): RowActionsMenuItem[] => [\n // Offered only where a plugin publishes a lead's address: text-less\n // rather than a link to a page this workspace cannot open.\n ...(leadHref(lead)\n ? [\n {\n key: 'crm',\n label: 'Open in CRM',\n icon: <MdiIcon path={mdiAccountArrowRight.path} size={0.8} />,\n href: leadHref(lead) as string,\n },\n ]\n : []),\n // A lead no site captured has no site to ask for its campaign.\n ...(originSite(lead)\n ? [\n {\n key: 'origin',\n label: 'Where this came from',\n icon: <MdiIcon path={mdiBullhornOutline.path} size={0.8} />,\n onClick: () => setLeadOrigin(lead),\n },\n ]\n : []),\n ]\n\n const columns: GridColDef[] = [\n {\n field: 'email',\n headerName: 'Email',\n flex: 1,\n minWidth: 240,\n /*\n The name beside the address — a member's `displayName`, and the name\n the lead writer stores (AGL-2303) — because a list of bare addresses is\n a list nobody recognizes anyone in.\n */\n renderCell: ({ row: contact }) => {\n const name = view === 'members' ? contact.displayName : contact.name\n return (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'baseline', minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {contact.email}\n </Typography>\n {name ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {name}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n /*\n WHERE A LEAD CAME FROM (AGL-2338): every surface that captured the\n person — a form, a booking, a lead added by hand — one chip each. A lead\n written before `sources` carries the one `source` it was captured by,\n and a row with neither simply shows none.\n */\n ...(view === 'leads'\n ? [\n {\n field: 'sources',\n headerName: 'Source',\n width: 200,\n sortable: false,\n valueGetter: (_value: unknown, lead: any) =>\n leadSources(lead).map(leadSourceLabel).join(', '),\n renderCell: ({ row: lead }: { row: any }) => (\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'center', height: '100%' }}>\n {[...new Set(leadSources(lead).map(leadSourceLabel))].map((label) => (\n <Chip key={label} label={label} size=\"small\" variant=\"outlined\" />\n ))}\n </Stack>\n ),\n } satisfies GridColDef,\n ]\n : []),\n /*\n KNOWN BY, on the organization's Inbox: every site that captured the\n person, as the CRM's organization Leads list names them. A lead is one\n org row, so \"which site\" is a set, never one answer.\n */\n ...(view === 'leads' && hostId == null\n ? [\n {\n field: 'capturedByHostIds',\n headerName: 'Site',\n flex: 1,\n minWidth: 160,\n sortable: false,\n renderCell: ({ row: contact }: { row: any }) =>\n orgSiteNames(orgMount, contact.capturedByHostIds),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'createdAt',\n headerName: view === 'members' ? 'Joined' : 'Captured',\n width: 200,\n renderCell: ({ row: contact }) =>\n contact.createdAt?.toDate?.().toLocaleString() ?? '--',\n },\n listActionsColumn(\n (contact) => (\n <ListRowActions\n label={String(contact.email ?? contact.$id)}\n items={view === 'members' ? memberActions(contact) : leadActions(contact)}\n />\n ),\n { width: 72 },\n ),\n ]\n\n const empty = shown.rows.length === 0 && !shown.hasMore && !filtering\n const emptyLabel =\n view === 'members'\n ? 'No members yet — visitors can join at /signup on your site.'\n : hostId == null\n ? 'No leads on any site yet.'\n : 'No leads yet — the forms and bookings that file leads add them here.'\n\n return (\n <>\n <CardDisplay\n header={hostId == null ? 'Leads' : 'Site Members & Leads'}\n help={pluginDocsHelp('forms', {\n anchor: '#filter-the-inbox',\n })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n <Stack spacing={1.5}>\n <Stack useFlexGap direction=\"row\" spacing={1.5} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n value={view}\n onChange={(_event, next: ContactsView | null) => switchView(next)}\n aria-label=\"Which people to list\"\n >\n {/*\n Disabled on the organization's Inbox rather than hidden: a\n member signs up to one site, and the line beside the toggle\n says how to reach a site's members, where a missing button\n would say nothing.\n */}\n <ToggleButton value=\"members\" disabled={hostId == null}>\n {'Members'}\n </ToggleButton>\n <ToggleButton value=\"leads\">{'Leads'}</ToggleButton>\n </ToggleButtonGroup>\n {hostId == null ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Every site’s leads, newest first. Members sign up to one ' +\n 'site each — choose a site to list its members.'}\n </Typography>\n ) : null}\n </Stack>\n {empty ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {emptyLabel}\n </Typography>\n ) : (\n <>\n <ListFilterChips\n fields={declaration.fields}\n headers={headers}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={options}\n />\n <ListQueryNotices refused={refusals} notices={notices} />\n <ListTable\n // One grid per list, so the other list's search box and\n // panel state never carry across the toggle.\n key={view}\n aria-label={view === 'members' ? 'Site members' : 'Leads'}\n rows={shown.rows}\n columns={listFilterGridColumns(columns, declaration.fields, options, headers)}\n rowHeight={TABLE_ROW_HEIGHT}\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 noRowsLabel={\n view === 'members'\n ? 'No members match these filters'\n : 'No leads match these filters'\n }\n />\n <ListPagination\n page={shown.page}\n pageSize={shown.pageSize}\n rowCount={shown.rows.length}\n hasMore={shown.hasMore}\n onPageChange={shown.setPage}\n onPageSizeChange={shown.setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n {/*\n WHERE A LEAD CAME FROM.\n\n Its own dialog rather than a column, for the reason the state above\n gives: one keyed read, paid by the reader who asked the question. A\n lead has no page of its own to put this on, and giving it one to carry\n a single line would be a new record surface rather than attribution.\n */}\n <Dialog\n open={Boolean(leadOrigin)}\n onClose={() => setLeadOrigin(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{leadOrigin?.email ?? 'Lead'}</DialogTitle>\n <DialogContent>\n {leadOrigin?.$id && leadOriginSite ? (\n <InboxRecordAttributionZone\n hostId={leadOriginSite}\n recordKind=\"lead\"\n recordId={String(leadOrigin.$id)}\n />\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button variant=\"contained\" onClick={() => setLeadOrigin(null)}>\n {'Close'}\n </Button>\n </DialogActions>\n </Dialog>\n </>\n )\n}\nContactsCard.displayName = 'ContactsCard'\n\nexport default ContactsCard\n"],"names":["pluginDocsHelp","InboxRecordAttributionZone","pluginRecordHref","scopeTokensForHost","mdiAccountArrowRight","mdiAccountRemoveOutline","mdiBullhornOutline","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","useSnackbar","authorizedFetch","useFirestore","useOrgDataScope","useScopeTokens","useUser","useListQuery","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Stack","ToggleButton","ToggleButtonGroup","Typography","collection","useCallback","useMemo","useState","LEAD_FILTER_HEADERS","LEAD_LIST_QUERY","LEAD_SOURCE_OPTIONS","ORG_LEAD_LIST_QUERY","SITE_MEMBER_FILTER_HEADERS","SITE_MEMBER_LIST_QUERY","leadListBase","LEAD_ADDRESS_SEARCH_COLUMN","LEAD_ADDRESS_SEARCH_NOTICE","leadListQueryFor","leadSourceLabel","orgSiteNames","useRecordRouteContext","ORG_LEAD_SELECT_FIELDS","NO_SELECT_FIELDS","leadSources","lead","Array","isArray","sources","filter","source","ContactsCard","hostId","orgMount","firestore","data","user","enqueueSnackbar","confirm","routeContext","leadHref","contact","String","$id","chosen","setChosen","view","searchWords","setSearchWords","gridFilter","selectFields","search","words","onChange","switchView","next","setClauses","request","clauses","members","declaration","deps","idField","orgId","undefined","reach","foldsScope","loaded","orgWide","leadDeclaration","leadBase","leadQuery","base","JSON","stringify","leads","shown","headers","options","capturedByHostIds","hosts","map","host","value","id","label","name","refusals","plan","refused","entry","clause","field","fields","addressSearched","addressSearch","served","some","notices","filtering","length","word","trim","handleDeleteMember","member","confirmed","title","description","email","confirmationText","confirmationButtonProps","color","then","catch","response","method","body","memberId","payload","json","ok","error","variant","allowDuplicate","persist","leadOrigin","setLeadOrigin","originSite","first","leadOriginSite","memberActions","key","icon","path","size","destructive","onClick","leadActions","href","columns","headerName","flex","minWidth","renderCell","row","displayName","direction","spacing","sx","alignItems","noWrap","width","sortable","valueGetter","_value","join","height","Set","createdAt","toDate","toLocaleString","items","empty","rows","hasMore","emptyLabel","header","help","anchor","contentGutterX","contentGutterY","contentBordered","useFlexGap","flexWrap","exclusive","_event","aria-label","disabled","rowHeight","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","page","pageSize","rowCount","onPageChange","setPage","onPageSizeChange","setPageSize","open","Boolean","onClose","maxWidth","fullWidth","recordKind","recordId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/contacts-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 { pluginDocsHelp, type ConsolePluginOrgMount } 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, not the plugin barrel: the barrel\n// carries the plugin registration, and a link needs only the address grammar.\nimport { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n mdiAccountArrowRight,\n mdiAccountRemoveOutline,\n mdiBullhornOutline,\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 { 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 { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n useFirestore,\n useOrgDataScope,\n useScopeTokens,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useListColumnSort } from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort'\nimport type { ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n LEAD_FILTER_HEADERS,\n LEAD_LIST_QUERY,\n LEAD_SOURCE_OPTIONS,\n ORG_LEAD_LIST_QUERY,\n SITE_MEMBER_FILTER_HEADERS,\n SITE_MEMBER_LIST_QUERY,\n leadListBase,\n LEAD_ADDRESS_SEARCH_COLUMN,\n LEAD_ADDRESS_SEARCH_NOTICE,\n leadListQueryFor,\n leadSourceLabel,\n} from '../constants/list-queries'\nimport { orgSiteNames } from './inbox-org-sites'\nimport { useRecordRouteContext } from './use-record-route-context'\n\n/** Which of the two collections the card lists. */\nexport type ContactsView = 'members' | 'leads'\n\n/** The organization's Inbox's Source and Site are picked, so the panel shows selects. */\nconst ORG_LEAD_SELECT_FIELDS = ['sources', 'capturedByHostIds']\nconst NO_SELECT_FIELDS: readonly string[] = []\n\n/**\n * Every surface a lead was captured by: `sources`, which every capture\n * `arrayUnion`s, or the single `source` a lead written before it carries.\n */\nconst leadSources = (lead: any): string[] =>\n Array.isArray(lead?.sources)\n ? lead.sources.filter((source: unknown): source is string => typeof source === 'string')\n : typeof lead?.source === 'string' && lead.source\n ? [lead.source]\n : []\n\n/**\n * The Members & leads section of the Inbox (AGL-109): the people a site\n * collected — its members, who signed up to it, and its leads, whom a form,\n * a booking or a teammate captured.\n *\n * ## Two lists, one table (AGL-3321)\n *\n * Members and leads are two collections, and the table shows one at a time,\n * chosen by the toggle above it. Each is its own Firestore query, every\n * filter and search word on it, paged by the server — so a page is a page of\n * the answer, and a site with thousands of either reaches all of them.\n *\n * It used to read the two hundred newest of each and interleave them, hiding\n * a lead whose address matched a member. That dedupe is only correct while\n * both reads are whole, which is exactly what a paged query is not, so it is\n * gone: a person who left their address and later signed up is a lead in one\n * list and a member in the other, which is what they are in each collection.\n *\n * On the organization's Inbox (`hostId: null`, AGL-3303) it lists the\n * organization's leads with the sites each was captured by. A member signs up\n * to ONE site and lives under it, so Members waits for a site to be picked,\n * which is the page handing this card that site.\n */\nexport function ContactsCard({\n hostId,\n orgMount,\n}: {\n hostId: string | null\n /** The organization and its sites — present exactly when `hostId` is `null`. */\n orgMount?: ConsolePluginOrgMount\n}) {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // Where a lead is WORKED (AGL-2608). This card lists leads; the CRM's\n // Leads section gives each one a status, an owner and a conversion.\n const routeContext = useRecordRouteContext()\n const leadHref = (contact: { $id?: unknown }): string | null =>\n routeContext\n ? pluginRecordHref('lead', routeContext, String(contact.$id))\n : null\n\n /*\n * The list shown. Members exist only under a site, so the organization's\n * Inbox shows leads whatever was chosen, and the toggle says why.\n */\n const [chosen, setChosen] = useState<ContactsView>('members')\n const view: ContactsView = hostId ? chosen : 'leads'\n const [searchWords, setSearchWords] = useState<string[]>([])\n const gridFilter = useListGridFilter({\n selectFields: view === 'leads' && hostId == null ? ORG_LEAD_SELECT_FIELDS : NO_SELECT_FIELDS,\n search: { words: searchWords, onChange: setSearchWords },\n })\n /*\n * EVERY HEADER SORTS (AGL-3680). Email and Joined/Captured order the QUERY\n * (`contactSorts`): both are on every member and lead. Source and Site are\n * sets on a lead, so they sort the page on screen and say so. Null is the\n * list's own order, newest first; a switch of list starts it again.\n */\n const [askedSort, setAskedSort] = useState<ListQuerySort | null>(null)\n /*\n * A switch of list starts it clean: the two lists filter by different\n * fields, and a clause carried across would be one the other list cannot\n * ask — refused on arrival, for a question the reader asked of the list\n * they just left.\n */\n const switchView = (next: ContactsView | null) => {\n if (!next || next === view) return\n gridFilter.setClauses([])\n setSearchWords([])\n setAskedSort(null)\n setChosen(next)\n }\n const request = {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n sort: askedSort,\n }\n\n /*==========================================\n * SITE MEMBERS — `hosts/{hostId}/siteMembers`, newest first\n * (`SITE_MEMBER_LIST_QUERY`). `createdAt` is safe to order on:\n * `membership-register.ts` is the only writer that CREATES a site member\n * and stamps it inside its transaction, and the collection is not in\n * `IMPORTABLE_FIELDS`. No query opens while the other list is shown.\n *=========================================*/\n const members = useListQuery<any>({\n collection:\n hostId && view === 'members'\n ? collection(firestore, 'hosts', hostId, 'siteMembers')\n : null,\n declaration: SITE_MEMBER_LIST_QUERY,\n request: view === 'members' ? request : { clauses: [] },\n deps: [firestore, hostId, view],\n idField: '$id',\n })\n\n /*==========================================\n * LEADS — `orgs/{orgId}/leads`, newest first (`LEAD_LIST_QUERY`). The\n * silo is the org's (AGL-3275), narrowed to this site by `visibleTo`;\n * reading `hosts/{hostId}/leads` showed a site its pre-migration rows and\n * nothing captured since. `addHostLead`, every lead door's one writer,\n * stamps `createdAt` on every create.\n *\n * Every site's, on the organization's Inbox: an org-wide member reads the\n * collection unscoped — the rules short-circuit on their reach, as for the\n * CRM's own org list — so a clause there would only narrow what they may\n * already read.\n *=========================================*/\n const { orgId } = useOrgDataScope({\n hostId: hostId ?? undefined,\n orgId: hostId == null ? orgMount?.orgId : undefined,\n })\n /*\n * Whether the search may fold into the scope clause (`scopedSearch`): a\n * query without `visibleTo` is one the rules prove only for an ORG-WIDE\n * member. Anyone else keeps the clause, and the plan refuses their search\n * by name rather than sending a query the rules deny. Until the reach has\n * answered, the answer is no. Asked only while Leads is shown: the member\n * document is a read, and Members has no scope to fold into.\n */\n const reach = useScopeTokens(hostId && orgId && view === 'leads' ? orgId : undefined)\n const foldsScope = hostId == null || (reach.loaded && reach.orgWide)\n const leadDeclaration = hostId ? LEAD_LIST_QUERY : ORG_LEAD_LIST_QUERY\n const leadBase = useMemo(\n () => leadListBase(hostId ? scopeTokensForHost(hostId) : null),\n [hostId],\n )\n const leadQuery = useMemo(\n () =>\n leadListQueryFor(\n leadDeclaration,\n foldsScope,\n view === 'leads' ? { ...request, base: leadBase } : { clauses: [] },\n ),\n // The request is data; its JSON is its identity.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [leadDeclaration, foldsScope, view, leadBase, JSON.stringify(request)],\n )\n const leads = useListQuery<any>({\n collection:\n orgId && view === 'leads' ? collection(firestore, 'orgs', orgId, 'leads') : null,\n declaration: leadQuery.declaration,\n request: leadQuery.request,\n deps: [firestore, orgId, hostId, view],\n idField: '$id',\n })\n\n const shown = view === 'members' ? members : leads\n const declaration = view === 'members' ? SITE_MEMBER_LIST_QUERY : leadDeclaration\n const headers = view === 'members' ? SITE_MEMBER_FILTER_HEADERS : LEAD_FILTER_HEADERS\n const options = useMemo<Readonly<Record<string, readonly ListFilterOption[]>>>(\n () =>\n view === 'leads' && hostId == null\n ? {\n sources: LEAD_SOURCE_OPTIONS,\n capturedByHostIds: (orgMount?.hosts ?? []).map((host) => ({\n value: host.id,\n label: host.name || host.id,\n })),\n }\n : {},\n [view, hostId, orgMount],\n )\n /*\n * A site collaborator's search rides the query as an address-prefix clause\n * (`leadListQueryFor`); a refusal of it is the search's, and when it is\n * served the list says what it matched and how it is now ordered.\n */\n const refusals = useMemo(\n () =>\n listQueryRefusals(\n shown.plan.refused.map((entry) =>\n entry.clause !== 'search' && entry.clause.field === LEAD_ADDRESS_SEARCH_COLUMN\n ? { ...entry, clause: 'search' as const }\n : entry,\n ),\n { fields: declaration.fields, headers, options },\n ),\n [shown.plan.refused, declaration, headers, options],\n )\n const addressSearched =\n view === 'leads' &&\n leadQuery.addressSearch &&\n leads.plan.served.some((clause) => clause.field === LEAD_ADDRESS_SEARCH_COLUMN)\n const notices = useMemo(\n () => (addressSearched ? [...shown.plan.notices, LEAD_ADDRESS_SEARCH_NOTICE] : shown.plan.notices),\n [addressSearched, shown.plan.notices],\n )\n const contactPageSorts = useMemo(\n () => ({\n sources: (lead: any) => leadSources(lead).map(leadSourceLabel).join(', '),\n capturedByHostIds: (contact: any) => orgSiteNames(orgMount, contact.capturedByHostIds),\n }),\n [orgMount],\n )\n const columnSort = useListColumnSort<any>({\n sorts: declaration.sorts,\n defaultSort: declaration.sorts[0],\n sort: askedSort,\n onSortChange: setAskedSort,\n orderBy: shown.plan.orderBy,\n rows: shown.rows,\n pageSorts: view === 'leads' ? contactPageSorts : undefined,\n headers,\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n\n /*\n * REMOVED BY THE ROUTE THAT OWNS MEMBER ACCOUNTS (AGL-3308), not by a\n * client delete. A member's password hash lives in a document no client can\n * reach, so deleting the profile from here would leave it behind: the route\n * deletes both, and the rules refuse a client delete of any member that\n * has one.\n */\n const handleDeleteMember = useCallback(\n (member: any) => async () => {\n // Members are listed only under a site, so there is one to delete from.\n if (!hostId) return\n const confirmed = await confirm({\n title: 'Remove this member?',\n description: `\"${member.email}\" can no longer sign in to your site.`,\n confirmationText: 'Remove',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n const response = await authorizedFetch(user, '/api/membership/admin-remove', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, memberId: member.$id }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(payload?.error ?? 'The member was not removed', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar('Member removed', { variant: 'success', persist: false })\n } catch {\n enqueueSnackbar('The member was not removed', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n },\n [confirm, user, hostId, enqueueSnackbar],\n )\n\n /*\n * WHERE A LEAD CAME FROM, on request.\n *\n * A lead is a table row with no page of its own, and the attribution is one\n * keyed document read — cheap on its own, and the page size times cheap in\n * a column. So it is an overflow action that opens a dialog: the reader who\n * wants the answer pays for it, and the reader who came to scan the list\n * does not.\n */\n const [leadOrigin, setLeadOrigin] = useState<any | null>(null)\n /*\n * The site a lead's attribution is asked of. Under a site it is that site;\n * across every site a lead is one org row that several sites may have met,\n * and the credit is filed by the site whose capture made it — the first in\n * `capturedByHostIds`, the site the CRM's own lead page asks as well.\n */\n const originSite = (contact: any): string | null => {\n if (hostId) return hostId\n const first = Array.isArray(contact?.capturedByHostIds)\n ? contact.capturedByHostIds[0]\n : null\n return typeof first === 'string' && first ? first : null\n }\n const leadOriginSite = leadOrigin ? originSite(leadOrigin) : null\n\n const memberActions = (member: any): RowActionsMenuItem[] => [\n {\n key: 'remove',\n label: 'Remove member',\n icon: <MdiIcon path={mdiAccountRemoveOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDeleteMember(member)(),\n },\n ]\n const leadActions = (lead: any): RowActionsMenuItem[] => [\n // Offered only where a plugin publishes a lead's address: text-less\n // rather than a link to a page this workspace cannot open.\n ...(leadHref(lead)\n ? [\n {\n key: 'crm',\n label: 'Open in CRM',\n icon: <MdiIcon path={mdiAccountArrowRight.path} size={0.8} />,\n href: leadHref(lead) as string,\n },\n ]\n : []),\n // A lead no site captured has no site to ask for its campaign.\n ...(originSite(lead)\n ? [\n {\n key: 'origin',\n label: 'Where this came from',\n icon: <MdiIcon path={mdiBullhornOutline.path} size={0.8} />,\n onClick: () => setLeadOrigin(lead),\n },\n ]\n : []),\n ]\n\n const columns: GridColDef[] = [\n {\n field: 'email',\n headerName: 'Email',\n flex: 1,\n minWidth: 240,\n /*\n The name beside the address — a member's `displayName`, and the name\n the lead writer stores (AGL-2303) — because a list of bare addresses is\n a list nobody recognizes anyone in.\n */\n renderCell: ({ row: contact }) => {\n const name = view === 'members' ? contact.displayName : contact.name\n return (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'baseline', minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {contact.email}\n </Typography>\n {name ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {name}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n /*\n WHERE A LEAD CAME FROM (AGL-2338): every surface that captured the\n person — a form, a booking, a lead added by hand — one chip each. A lead\n written before `sources` carries the one `source` it was captured by,\n and a row with neither simply shows none.\n */\n ...(view === 'leads'\n ? [\n {\n field: 'sources',\n headerName: 'Source',\n width: 200,\n valueGetter: (_value: unknown, lead: any) =>\n leadSources(lead).map(leadSourceLabel).join(', '),\n renderCell: ({ row: lead }: { row: any }) => (\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'center', height: '100%' }}>\n {[...new Set(leadSources(lead).map(leadSourceLabel))].map((label) => (\n <Chip key={label} label={label} size=\"small\" variant=\"outlined\" />\n ))}\n </Stack>\n ),\n } satisfies GridColDef,\n ]\n : []),\n /*\n KNOWN BY, on the organization's Inbox: every site that captured the\n person, as the CRM's organization Leads list names them. A lead is one\n org row, so \"which site\" is a set, never one answer.\n */\n ...(view === 'leads' && hostId == null\n ? [\n {\n field: 'capturedByHostIds',\n headerName: 'Site',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row: contact }: { row: any }) =>\n orgSiteNames(orgMount, contact.capturedByHostIds),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'createdAt',\n headerName: view === 'members' ? 'Joined' : 'Captured',\n width: 200,\n renderCell: ({ row: contact }) =>\n contact.createdAt?.toDate?.().toLocaleString() ?? '--',\n },\n listActionsColumn(\n (contact) => (\n <ListRowActions\n label={String(contact.email ?? contact.$id)}\n items={view === 'members' ? memberActions(contact) : leadActions(contact)}\n />\n ),\n { width: 72 },\n ),\n ]\n\n const empty = shown.rows.length === 0 && !shown.hasMore && !filtering\n const emptyLabel =\n view === 'members'\n ? 'No members yet — visitors can join at /signup on your site.'\n : hostId == null\n ? 'No leads on any site yet.'\n : 'No leads yet — the forms and bookings that file leads add them here.'\n\n return (\n <>\n <CardDisplay\n header={hostId == null ? 'Leads' : 'Site Members & Leads'}\n help={pluginDocsHelp('forms', {\n anchor: '#filter-the-inbox',\n })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n <Stack spacing={1.5}>\n <Stack useFlexGap direction=\"row\" spacing={1.5} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n value={view}\n onChange={(_event, next: ContactsView | null) => switchView(next)}\n aria-label=\"Which people to list\"\n >\n {/*\n Disabled on the organization's Inbox rather than hidden: a\n member signs up to one site, and the line beside the toggle\n says how to reach a site's members, where a missing button\n would say nothing.\n */}\n <ToggleButton value=\"members\" disabled={hostId == null}>\n {'Members'}\n </ToggleButton>\n <ToggleButton value=\"leads\">{'Leads'}</ToggleButton>\n </ToggleButtonGroup>\n {hostId == null ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Every site’s leads, newest first. Members sign up to one ' +\n 'site each — choose a site to list its members.'}\n </Typography>\n ) : null}\n </Stack>\n {empty ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {emptyLabel}\n </Typography>\n ) : (\n <>\n <ListFilterChips\n fields={declaration.fields}\n headers={headers}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={options}\n />\n <ListQueryNotices\n refused={refusals}\n notices={[...notices, ...columnSort.notices]}\n />\n <ListTable\n // One grid per list, so the other list's search box and\n // panel state never carry across the toggle.\n key={view}\n aria-label={view === 'members' ? 'Site members' : 'Leads'}\n rows={columnSort.rows}\n columnSort={columnSort}\n columns={listFilterGridColumns(columns, declaration.fields, options, headers)}\n rowHeight={TABLE_ROW_HEIGHT}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel, the search and the headers go to the query\n // (Source and Site sort the page, and say so).\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel={\n view === 'members'\n ? 'No members match these filters'\n : 'No leads match these filters'\n }\n />\n <ListPagination\n page={shown.page}\n pageSize={shown.pageSize}\n rowCount={shown.rows.length}\n hasMore={shown.hasMore}\n onPageChange={shown.setPage}\n onPageSizeChange={shown.setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n {/*\n WHERE A LEAD CAME FROM.\n\n Its own dialog rather than a column, for the reason the state above\n gives: one keyed read, paid by the reader who asked the question. A\n lead has no page of its own to put this on, and giving it one to carry\n a single line would be a new record surface rather than attribution.\n */}\n <Dialog\n open={Boolean(leadOrigin)}\n onClose={() => setLeadOrigin(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{leadOrigin?.email ?? 'Lead'}</DialogTitle>\n <DialogContent>\n {leadOrigin?.$id && leadOriginSite ? (\n <InboxRecordAttributionZone\n hostId={leadOriginSite}\n recordKind=\"lead\"\n recordId={String(leadOrigin.$id)}\n />\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button variant=\"contained\" onClick={() => setLeadOrigin(null)}>\n {'Close'}\n </Button>\n </DialogActions>\n </Dialog>\n </>\n )\n}\nContactsCard.displayName = 'ContactsCard'\n\nexport default ContactsCard\n"],"names":["pluginDocsHelp","InboxRecordAttributionZone","pluginRecordHref","scopeTokensForHost","mdiAccountArrowRight","mdiAccountRemoveOutline","mdiBullhornOutline","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","useSnackbar","authorizedFetch","useFirestore","useOrgDataScope","useScopeTokens","useUser","useListQuery","useListColumnSort","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Stack","ToggleButton","ToggleButtonGroup","Typography","collection","useCallback","useMemo","useState","LEAD_FILTER_HEADERS","LEAD_LIST_QUERY","LEAD_SOURCE_OPTIONS","ORG_LEAD_LIST_QUERY","SITE_MEMBER_FILTER_HEADERS","SITE_MEMBER_LIST_QUERY","leadListBase","LEAD_ADDRESS_SEARCH_COLUMN","LEAD_ADDRESS_SEARCH_NOTICE","leadListQueryFor","leadSourceLabel","orgSiteNames","useRecordRouteContext","ORG_LEAD_SELECT_FIELDS","NO_SELECT_FIELDS","leadSources","lead","Array","isArray","sources","filter","source","ContactsCard","hostId","orgMount","firestore","data","user","enqueueSnackbar","confirm","routeContext","leadHref","contact","String","$id","chosen","setChosen","view","searchWords","setSearchWords","gridFilter","selectFields","search","words","onChange","askedSort","setAskedSort","switchView","next","setClauses","request","clauses","sort","members","declaration","deps","idField","orgId","undefined","reach","foldsScope","loaded","orgWide","leadDeclaration","leadBase","leadQuery","base","JSON","stringify","leads","shown","headers","options","capturedByHostIds","hosts","map","host","value","id","label","name","refusals","plan","refused","entry","clause","field","fields","addressSearched","addressSearch","served","some","notices","contactPageSorts","join","columnSort","sorts","defaultSort","onSortChange","orderBy","rows","pageSorts","filtering","length","word","trim","handleDeleteMember","member","confirmed","title","description","email","confirmationText","confirmationButtonProps","color","then","catch","response","method","body","memberId","payload","json","ok","error","variant","allowDuplicate","persist","leadOrigin","setLeadOrigin","originSite","first","leadOriginSite","memberActions","key","icon","path","size","destructive","onClick","leadActions","href","columns","headerName","flex","minWidth","renderCell","row","displayName","direction","spacing","sx","alignItems","noWrap","width","valueGetter","_value","height","Set","createdAt","toDate","toLocaleString","items","empty","hasMore","emptyLabel","header","help","anchor","contentGutterX","contentGutterY","contentBordered","useFlexGap","flexWrap","exclusive","_event","aria-label","disabled","rowHeight","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","page","pageSize","rowCount","onPageChange","setPage","onPageSizeChange","setPageSize","open","Boolean","onClose","maxWidth","fullWidth","recordKind","recordId"],"mappings":"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"}
|
|
@@ -41,6 +41,7 @@ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filt
|
|
|
41
41
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
42
42
|
import { useFirestore, useFirestoreCollection, usePluginApiPost } from "@aglyn/tenant-feature-instance";
|
|
43
43
|
import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
44
|
+
import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
|
|
44
45
|
import { Avatar, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, Stack, Tooltip, Typography } from "@mui/material";
|
|
45
46
|
import { collection, deleteDoc, getDoc, limit, orderBy, query, updateDoc } from "firebase/firestore";
|
|
46
47
|
// The submissions are the forms plugin's records, walked and opened through
|
|
@@ -68,7 +69,12 @@ const SUBMISSION_HIDDEN_COLUMNS = {
|
|
|
68
69
|
read: false,
|
|
69
70
|
formId: false
|
|
70
71
|
};
|
|
71
|
-
/** Every field of a message, as one line — what the Message column draws. */ const
|
|
72
|
+
/** Every field of a message, as one line — what the Message column draws. */ /** The page-sorted headers, for their notice (AGL-3680). */ const SUBMISSION_PAGE_SORT_HEADERS = {
|
|
73
|
+
from: 'From',
|
|
74
|
+
hostId: 'Site',
|
|
75
|
+
message: 'Message'
|
|
76
|
+
};
|
|
77
|
+
const messageTextOf = (submission)=>{
|
|
72
78
|
var _ref;
|
|
73
79
|
return Object.entries((_ref = submission == null ? void 0 : submission.fields) != null ? _ref : {}).map(([key, value])=>`${key}: ${value}`).join(' · ');
|
|
74
80
|
};
|
|
@@ -162,6 +168,12 @@ export function SubmissionsCard({ hostId, formId, orgMount }) {
|
|
|
162
168
|
selectFields: SUBMISSION_SELECT_FIELDS
|
|
163
169
|
});
|
|
164
170
|
/*
|
|
171
|
+
* EVERY HEADER SORTS (AGL-3680). Received and Read order the QUERY (see
|
|
172
|
+
* `SUBMISSION_LIST_QUERY`); From, Site and Message are drawn from the
|
|
173
|
+
* message, so they sort the page on screen and say so. Null is newest
|
|
174
|
+
* first, the inbox's own order.
|
|
175
|
+
*/ const [askedSort, setAskedSort] = useState(null);
|
|
176
|
+
/*
|
|
165
177
|
* The inbox WALKS its submissions instead of sampling them (AGL-2501,
|
|
166
178
|
* AGL-2292), and every filter and search word is a predicate on the walk
|
|
167
179
|
* (AGL-3321), so a page is a page of the matches.
|
|
@@ -197,6 +209,7 @@ export function SubmissionsCard({ hostId, formId, orgMount }) {
|
|
|
197
209
|
request: {
|
|
198
210
|
clauses: gridFilter.clauses,
|
|
199
211
|
search: gridFilter.searchWords,
|
|
212
|
+
sort: askedSort,
|
|
200
213
|
base
|
|
201
214
|
},
|
|
202
215
|
deps: [
|
|
@@ -236,6 +249,23 @@ export function SubmissionsCard({ hostId, formId, orgMount }) {
|
|
|
236
249
|
declaration,
|
|
237
250
|
submissionOptions
|
|
238
251
|
]);
|
|
252
|
+
const submissionPageSorts = useMemo(()=>({
|
|
253
|
+
from: (submission)=>submissionSender(submission.fields).label,
|
|
254
|
+
hostId: (submission)=>orgSiteName(orgMount, submission.hostId),
|
|
255
|
+
message: (submission)=>messageTextOf(submission)
|
|
256
|
+
}), [
|
|
257
|
+
orgMount
|
|
258
|
+
]);
|
|
259
|
+
const columnSort = useListColumnSort({
|
|
260
|
+
sorts: declaration.sorts,
|
|
261
|
+
defaultSort: declaration.sorts[0],
|
|
262
|
+
sort: askedSort,
|
|
263
|
+
onSortChange: setAskedSort,
|
|
264
|
+
orderBy: plan.orderBy,
|
|
265
|
+
rows: submissions,
|
|
266
|
+
pageSorts: submissionPageSorts,
|
|
267
|
+
headers: SUBMISSION_PAGE_SORT_HEADERS
|
|
268
|
+
});
|
|
239
269
|
const searching = gridFilter.searchWords.some((word)=>word.trim());
|
|
240
270
|
const filtering = gridFilter.clauses.length > 0 || searching;
|
|
241
271
|
/** Narrowed by nothing but a Form clause. */ const onlyForm = !searching && gridFilter.clauses.length > 0 && gridFilter.clauses.every((clause)=>clause.field === 'formId');
|
|
@@ -606,11 +636,15 @@ export function SubmissionsCard({ hostId, formId, orgMount }) {
|
|
|
606
636
|
}),
|
|
607
637
|
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
608
638
|
refused: refusals,
|
|
609
|
-
notices:
|
|
639
|
+
notices: [
|
|
640
|
+
...plan.notices,
|
|
641
|
+
...columnSort.notices
|
|
642
|
+
]
|
|
610
643
|
}),
|
|
611
644
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
612
645
|
"aria-label": scoped ? 'Submissions to this form' : 'Form submissions',
|
|
613
|
-
rows:
|
|
646
|
+
rows: columnSort.rows,
|
|
647
|
+
columnSort: columnSort,
|
|
614
648
|
columns: listFilterGridColumns(submissionColumns, declaration.fields, submissionOptions, SUBMISSION_FILTER_HEADERS),
|
|
615
649
|
initialState: {
|
|
616
650
|
columns: {
|
|
@@ -630,13 +664,12 @@ export function SubmissionsCard({ hostId, formId, orgMount }) {
|
|
|
630
664
|
},
|
|
631
665
|
// Paged by the footer below, so the grid must not also slice.
|
|
632
666
|
hideFooter: true,
|
|
633
|
-
// The panel and the
|
|
634
|
-
//
|
|
667
|
+
// The panel, the search and the headers go to the query (From,
|
|
668
|
+
// Site and Message sort the page, and say so).
|
|
635
669
|
filterMode: "server",
|
|
636
670
|
filterModel: gridFilter.filterModel,
|
|
637
671
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
638
|
-
quickFilter: true
|
|
639
|
-
disableColumnSorting: true
|
|
672
|
+
quickFilter: true
|
|
640
673
|
}),
|
|
641
674
|
/*#__PURE__*/ _jsx(ListPagination, {
|
|
642
675
|
page: submissionPage,
|
|
@@ -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'\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 useFlexGap\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","useFlexGap","flexWrap","rowGap","routing","chip","recordKind","recordId","filedUnder"],"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 { useListColumnSort } from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort'\nimport type { ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\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. */\n/** The page-sorted headers, for their notice (AGL-3680). */\nconst SUBMISSION_PAGE_SORT_HEADERS = { from: 'From', hostId: 'Site', message: 'Message' }\n\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 * EVERY HEADER SORTS (AGL-3680). Received and Read order the QUERY (see\n * `SUBMISSION_LIST_QUERY`); From, Site and Message are drawn from the\n * message, so they sort the page on screen and say so. Null is newest\n * first, the inbox's own order.\n */\n const [askedSort, setAskedSort] = useState<ListQuerySort | null>(null)\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: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n sort: askedSort,\n base,\n },\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 submissionPageSorts = useMemo(\n () => ({\n from: (submission: any) => submissionSender(submission.fields).label,\n hostId: (submission: any) => orgSiteName(orgMount, submission.hostId),\n message: (submission: any) => messageTextOf(submission),\n }),\n [orgMount],\n )\n const columnSort = useListColumnSort<any>({\n sorts: declaration.sorts,\n defaultSort: declaration.sorts[0],\n sort: askedSort,\n onSortChange: setAskedSort,\n orderBy: plan.orderBy,\n rows: submissions,\n pageSorts: submissionPageSorts,\n headers: SUBMISSION_PAGE_SORT_HEADERS,\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\n refused={refusals}\n notices={[...plan.notices, ...columnSort.notices]}\n />\n <ListTable\n aria-label={scoped ? 'Submissions to this form' : 'Form submissions'}\n rows={columnSort.rows}\n columnSort={columnSort}\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, the search and the headers go to the query (From,\n // Site and Message sort the page, and say so).\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\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 useFlexGap\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","useListColumnSort","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","SUBMISSION_PAGE_SORT_HEADERS","from","hostId","message","messageTextOf","submission","Object","entries","fields","map","key","value","join","SubmissionsCard","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","askedSort","setAskedSort","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","submissionPageSorts","columnSort","sorts","defaultSort","onSortChange","pageSorts","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","rowCount","onPageChange","onPageSizeChange","open","onClose","maxWidth","fullWidth","mt","hrefOf","kind","siteOrigin","subdomain","find","link","external","target","rel","text","my","whiteSpace","wordBreak","useFlexGap","flexWrap","rowGap","routing","chip","recordKind","recordId","filedUnder"],"mappings":"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"}
|
|
@@ -24,7 +24,42 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
24
24
|
* combination one query cannot hold is refused by name. `list-queries.spec.ts`
|
|
25
25
|
* holds `cloud/firebase-firestore.indexes.json` to `listQueryIndexes` of each
|
|
26
26
|
* declaration.
|
|
27
|
-
*/
|
|
27
|
+
*/ /**
|
|
28
|
+
* The header orders of a newest-first contacts list (AGL-3680): newest first
|
|
29
|
+
* as the default, then oldest first and the address either way, each
|
|
30
|
+
* `alone` — served while nothing narrows the list past its scope, so they
|
|
31
|
+
* cost a composite only beside a scope clause. Both fields are on every row
|
|
32
|
+
* (see each list below).
|
|
33
|
+
*/ const contactSorts = (createdLabel)=>[
|
|
34
|
+
{
|
|
35
|
+
path: 'createdAt',
|
|
36
|
+
direction: 'desc',
|
|
37
|
+
column: 'createdAt',
|
|
38
|
+
label: createdLabel
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: 'createdAt',
|
|
42
|
+
direction: 'asc',
|
|
43
|
+
column: 'createdAt',
|
|
44
|
+
label: createdLabel,
|
|
45
|
+
alone: true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: 'email',
|
|
49
|
+
direction: 'asc',
|
|
50
|
+
column: 'email',
|
|
51
|
+
label: 'Email',
|
|
52
|
+
alone: true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: 'email',
|
|
56
|
+
direction: 'desc',
|
|
57
|
+
column: 'email',
|
|
58
|
+
label: 'Email',
|
|
59
|
+
alone: true
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
/*==========================================
|
|
28
63
|
* SITE MEMBERS — `hosts/{hostId}/siteMembers`, newest first.
|
|
29
64
|
*
|
|
30
65
|
* The same fields and index shapes as the console's Site users list
|
|
@@ -48,12 +83,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
48
83
|
]
|
|
49
84
|
}
|
|
50
85
|
],
|
|
51
|
-
sorts:
|
|
52
|
-
{
|
|
53
|
-
path: 'createdAt',
|
|
54
|
-
direction: 'desc'
|
|
55
|
-
}
|
|
56
|
-
],
|
|
86
|
+
sorts: contactSorts('Joined'),
|
|
57
87
|
search: {
|
|
58
88
|
tokensPath: 'searchTokens'
|
|
59
89
|
}
|
|
@@ -94,12 +124,7 @@ export const LEAD_LIST_QUERY = {
|
|
|
94
124
|
fields: [
|
|
95
125
|
LEAD_EMAIL_FIELD
|
|
96
126
|
],
|
|
97
|
-
sorts:
|
|
98
|
-
{
|
|
99
|
-
path: 'createdAt',
|
|
100
|
-
direction: 'desc'
|
|
101
|
-
}
|
|
102
|
-
],
|
|
127
|
+
sorts: contactSorts('Captured'),
|
|
103
128
|
search: {
|
|
104
129
|
tokensPath: 'searchTokens',
|
|
105
130
|
scoped: scopedSearch('scopedSearchTokens')
|
|
@@ -305,10 +330,38 @@ export const LEAD_FILTER_HEADERS = {
|
|
|
305
330
|
'isAnyOf'
|
|
306
331
|
]
|
|
307
332
|
};
|
|
308
|
-
|
|
333
|
+
/*
|
|
334
|
+
* Received orders the query either way, and Read either way — unread first
|
|
335
|
+
* (AGL-3680); both are on every row (see above). The headers past the
|
|
336
|
+
* default are `alone`, served while nothing narrows the list past its scope.
|
|
337
|
+
* From, Site and Message are drawn from the message, so they sort the page.
|
|
338
|
+
*/ const SUBMISSION_SORTS = [
|
|
339
|
+
{
|
|
340
|
+
path: 'createdAt',
|
|
341
|
+
direction: 'desc',
|
|
342
|
+
column: 'createdAt',
|
|
343
|
+
label: 'Received'
|
|
344
|
+
},
|
|
309
345
|
{
|
|
310
346
|
path: 'createdAt',
|
|
311
|
-
direction: '
|
|
347
|
+
direction: 'asc',
|
|
348
|
+
column: 'createdAt',
|
|
349
|
+
label: 'Received',
|
|
350
|
+
alone: true
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
path: 'read',
|
|
354
|
+
direction: 'asc',
|
|
355
|
+
column: 'read',
|
|
356
|
+
label: 'Read',
|
|
357
|
+
alone: true
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
path: 'read',
|
|
361
|
+
direction: 'desc',
|
|
362
|
+
column: 'read',
|
|
363
|
+
label: 'Read',
|
|
364
|
+
alone: true
|
|
312
365
|
}
|
|
313
366
|
];
|
|
314
367
|
const SUBMISSION_SEARCH = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/constants/list-queries.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 { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryRequest,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * WHAT EACH INBOX LIST CAN BE ASKED, ON ITS FIRESTORE QUERY (AGL-3321).\n *\n * Every clause the Filters panel offers and every quick-search word is a\n * predicate on the list's query, planned by `planListQuery`, so each page is\n * a page of the answer. A question no query can answer is not offered, and a\n * combination one query cannot hold is refused by name. `list-queries.spec.ts`\n * holds `cloud/firebase-firestore.indexes.json` to `listQueryIndexes` of each\n * declaration.\n */\n\n/*==========================================\n * SITE MEMBERS — `hosts/{hostId}/siteMembers`, newest first.\n *\n * The same fields and index shapes as the console's Site users list\n * (`apps/console/utils/site-account-query.ts`), so the composites are one\n * set. The search reads `searchTokens` — the start of any word of the\n * display name or of the address — which both writers of a member stamp\n * (`memberSearchTokens`, the commerce plugin's register and account routes)\n * and `backfill-site-member-search.mjs` stamps on the members written\n * before it. The register path lower-cases `email` before it stores it, so\n * the stored address is its own normalized key.\n *=========================================*/\nexport const SITE_MEMBER_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals'],\n },\n ],\n sorts: [{ path: 'createdAt', direction: 'desc' }],\n search: { tokensPath: 'searchTokens' },\n}\n\nexport const SITE_MEMBER_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Email',\n}\n\n/*==========================================\n * LEADS — `orgs/{orgId}/leads`, newest first.\n *\n * Under a site the list is narrowed to what that site may see\n * (`visibleTo array-contains-any`, {@link leadListBase}); on the\n * organization's Inbox an org-wide member reads it unscoped.\n *\n * `searchTokens` — the lead's name, address, company, title and tags as word\n * prefixes — and `scopedSearchTokens` — the same behind each `visibleTo`\n * token — are the CRM's list fields, stamped on every lead write by\n * `crmLeadListFields` (`@aglyn/aglyn`'s CRM app-utils) and backfilled by the\n * CRM's own lane. Under a site the search folds into the scope clause\n * (`scopedSearch`), which the rules prove only for an org-wide member: a\n * collaborator whose reach is some sites keeps the scope clause, and their\n * search is asked as the start of the address beside it\n * (`leadListQueryFor`).\n *\n * Every current lead writer normalizes the address (`normalizeContactEmail`)\n * before `addHostLead` stores it, so Email is an equality on `email`.\n *=========================================*/\nconst LEAD_EMAIL_FIELD: ListFilterField = {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals'],\n}\n\nexport const LEAD_LIST_QUERY: ListQueryDeclaration = {\n fields: [LEAD_EMAIL_FIELD],\n sorts: [{ path: 'createdAt', direction: 'desc' }],\n search: { tokensPath: 'searchTokens', scoped: scopedSearch('scopedSearchTokens') },\n}\n\n/**\n * The organization's Inbox adds what an unscoped read can ask: the surface\n * that captured the lead and the sites that did. Both are ARRAYS on the\n * lead (`sources`, `capturedByHostIds`, `arrayUnion`ed by `addHostLead`), so\n * each is the query's one array clause — \"any of\" — and cannot stand beside\n * the search or the other. Under a site the scope clause already holds that\n * one array clause, which is why neither is offered there.\n */\nexport const ORG_LEAD_LIST_QUERY: ListQueryDeclaration = {\n ...LEAD_LIST_QUERY,\n fields: [\n LEAD_EMAIL_FIELD,\n {\n column: 'sources',\n kind: 'exact',\n path: 'sources',\n tokensPath: 'sources',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n {\n column: 'capturedByHostIds',\n kind: 'exact',\n path: 'capturedByHostIds',\n tokensPath: 'capturedByHostIds',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n ],\n}\n\n/**\n * The column a site collaborator's search is asked through: the START of the\n * address, a prefix range on `email`. Not a Filters-panel field — the panel\n * reads the declaration the card shows, and this one rides only on the query.\n */\nexport const LEAD_ADDRESS_SEARCH_COLUMN = '$addressSearch'\n\nconst LEAD_ADDRESS_SEARCH_FIELD: ListFilterField = {\n column: LEAD_ADDRESS_SEARCH_COLUMN,\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n}\n\n/** The notice a site collaborator's search carries: what it matched, and the order. */\nexport const LEAD_ADDRESS_SEARCH_NOTICE =\n 'Search on this site’s leads matches the start of the address, and lists them by address.'\n\n/**\n * The query a reader may run for the Leads list, and whether its search was\n * asked as an address prefix (AGL-3321).\n *\n * An ORG-WIDE reader's search folds into the scope clause (`scopedSearch`):\n * a query without `visibleTo` is one the rules prove only for them. A reader\n * whose reach is some sites must keep `visibleTo array-contains-any` on the\n * query, which spends its one array clause — so their search is asked as a\n * prefix RANGE on the stored, normalized `email` beside the scope instead,\n * ordered by address (the `visibleTo, email` composite the lead list already\n * carries). Only the first word is asked.\n */\nexport function leadListQueryFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n request: ListQueryRequest,\n): { declaration: ListQueryDeclaration; request: ListQueryRequest; addressSearch: boolean } {\n if (foldsScope || !declaration.search?.scoped) return { declaration, request, addressSearch: false }\n const word = (request.search ?? []).join(' ').trim().split(/\\s+/)[0] ?? ''\n return {\n declaration: {\n ...declaration,\n fields: [...declaration.fields, LEAD_ADDRESS_SEARCH_FIELD],\n search: undefined,\n },\n request: {\n ...request,\n search: [],\n clauses: word\n ? [...request.clauses, { field: LEAD_ADDRESS_SEARCH_COLUMN, op: 'startsWith', value: word }]\n : request.clauses,\n },\n addressSearch: Boolean(word),\n }\n}\n\n/** The scope clause a site's lead list always carries, or none on the org's. */\nexport function leadListBase(scopeTokens: readonly string[] | null): ListQueryFilter[] {\n return scopeTokens ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...scopeTokens] }] : []\n}\n\n/** The base's shape for `listQueryIndexes`: the scope clause is an array clause. */\nexport const LEAD_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\nexport const LEAD_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Email',\n sources: 'Source',\n capturedByHostIds: 'Site',\n}\n\n/**\n * The capture surfaces a Source filter can name. A lead routed from a form\n * is filed as `form:{formId}` — one value per form, so no fixed choice can\n * ask for \"every form\" and none is offered; the rest are the fixed words the\n * doors write: a booking, a lead added by hand, over the API or from a file,\n * and `signup`, which member sign-ups wrote before they filed a contact.\n */\nexport const LEAD_SOURCE_OPTIONS: readonly ListFilterOption[] = [\n { value: 'booking', label: 'Booking' },\n { value: 'manual', label: 'Added by hand' },\n { value: 'api', label: 'API' },\n { value: 'import', label: 'Import' },\n { value: 'signup', label: 'Sign-up' },\n]\n\n/** How one capture surface reads on a row. */\nexport function leadSourceLabel(source: string): string {\n if (source === 'form' || source.startsWith('form:')) return 'Form'\n return LEAD_SOURCE_OPTIONS.find((option) => option.value === source)?.label ?? source\n}\n\n/*==========================================\n * FORM SUBMISSIONS — `hosts/{hostId}/formSubmissions` under a site, and the\n * `formSubmissions` collection group narrowed to `orgId` on the\n * organization's Inbox (the rules admit that group read only so narrowed).\n * Newest first.\n *\n * `senderTokens` and `searchTokens` are `messageSearchFields` of the\n * message, stamped by the submit route — the collection's only creator —\n * and by `backfill-form-submission-filters.mjs` on the rows before it; a\n * message's values never change after it arrives. `read` is a boolean on\n * every row: the route writes `false`, the reader and the API flip it, and\n * the same backfill stamps `false` on a row without one. `hostId` and\n * `orgId` are stamped by the route and frozen by the rules.\n *\n * ⛔ The form's NAME is not searched. The row keeps the name the form had\n * when the message arrived, and a rename would leave the search finding\n * rows by a name the reader no longer sees. Form is a filter by id instead,\n * which survives a rename.\n *=========================================*/\nconst SUBMISSION_FIELDS: readonly ListFilterField[] = [\n {\n column: 'from',\n kind: 'text',\n path: 'senderTokens',\n tokensPath: 'senderTokens',\n operators: ['contains'],\n },\n { column: 'read', kind: 'boolean', path: 'read', operators: ['equals'] },\n]\n\n/** Form: offered where a site's forms are read (not across every site). */\nconst FORM_FIELD: ListFilterField = {\n column: 'formId',\n kind: 'exact',\n path: 'formId',\n operators: ['equals'],\n}\n\n/** Site: the organization's Inbox only, where a row can be any site's. */\nconst SITE_FIELD: ListFilterField = {\n column: 'hostId',\n kind: 'exact',\n path: 'hostId',\n operators: ['equals', 'isAnyOf'],\n}\n\nconst SUBMISSION_SORTS = [{ path: 'createdAt', direction: 'desc' as const }]\nconst SUBMISSION_SEARCH = { tokensPath: 'searchTokens' }\n\n/** A site's Inbox. */\nexport const SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: [...SUBMISSION_FIELDS, FORM_FIELD],\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** A card already narrowed to one form, where Form has nothing to pick. */\nexport const FORM_SCOPED_SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: SUBMISSION_FIELDS,\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** Every site's, on the organization's Inbox. */\nexport const ORG_SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: [...SUBMISSION_FIELDS, SITE_FIELD],\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** The organization's Inbox's scope, which the rules require. */\nexport const orgSubmissionBase = (orgId: string): ListQueryFilter[] => [\n { path: 'orgId', op: '==', value: orgId },\n]\n\n/** A form-scoped card's scope. */\nexport const formSubmissionBase = (formId: string): ListQueryFilter[] => [\n { path: 'formId', op: '==', value: formId },\n]\n\nexport const SUBMISSION_FILTER_HEADERS: Readonly<Record<string, string>> = {\n from: 'From',\n read: 'Read',\n formId: 'Form',\n hostId: 'Site',\n}\n\n/** Read is the stored boolean, by the words a reader uses for it. */\nexport const SUBMISSION_READ_OPTIONS: readonly ListFilterOption[] = [\n { value: 'false', label: 'Unread' },\n { value: 'true', label: 'Read' },\n]\n"],"names":["scopedSearch","SITE_MEMBER_LIST_QUERY","fields","column","kind","path","lowerPath","presence","operators","sorts","direction","search","tokensPath","SITE_MEMBER_FILTER_HEADERS","email","LEAD_EMAIL_FIELD","LEAD_LIST_QUERY","scoped","ORG_LEAD_LIST_QUERY","verbatimTokens","LEAD_ADDRESS_SEARCH_COLUMN","LEAD_ADDRESS_SEARCH_FIELD","LEAD_ADDRESS_SEARCH_NOTICE","leadListQueryFor","declaration","foldsScope","request","addressSearch","word","join","trim","split","undefined","clauses","field","op","value","Boolean","leadListBase","scopeTokens","LEAD_LIST_BASE_INDEX","array","LEAD_FILTER_HEADERS","sources","capturedByHostIds","LEAD_SOURCE_OPTIONS","label","leadSourceLabel","source","startsWith","find","option","SUBMISSION_FIELDS","FORM_FIELD","SITE_FIELD","SUBMISSION_SORTS","SUBMISSION_SEARCH","SUBMISSION_LIST_QUERY","FORM_SCOPED_SUBMISSION_LIST_QUERY","ORG_SUBMISSION_LIST_QUERY","orgSubmissionBase","orgId","formSubmissionBase","formId","SUBMISSION_FILTER_HEADERS","from","read","hostId","SUBMISSION_READ_OPTIONS"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/constants/list-queries.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 { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryRequest,\n ListQuerySort,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * WHAT EACH INBOX LIST CAN BE ASKED, ON ITS FIRESTORE QUERY (AGL-3321).\n *\n * Every clause the Filters panel offers and every quick-search word is a\n * predicate on the list's query, planned by `planListQuery`, so each page is\n * a page of the answer. A question no query can answer is not offered, and a\n * combination one query cannot hold is refused by name. `list-queries.spec.ts`\n * holds `cloud/firebase-firestore.indexes.json` to `listQueryIndexes` of each\n * declaration.\n */\n\n/**\n * The header orders of a newest-first contacts list (AGL-3680): newest first\n * as the default, then oldest first and the address either way, each\n * `alone` — served while nothing narrows the list past its scope, so they\n * cost a composite only beside a scope clause. Both fields are on every row\n * (see each list below).\n */\nconst contactSorts = (createdLabel: string): ListQuerySort[] => [\n { path: 'createdAt', direction: 'desc', column: 'createdAt', label: createdLabel },\n { path: 'createdAt', direction: 'asc', column: 'createdAt', label: createdLabel, alone: true },\n { path: 'email', direction: 'asc', column: 'email', label: 'Email', alone: true },\n { path: 'email', direction: 'desc', column: 'email', label: 'Email', alone: true },\n]\n\n/*==========================================\n * SITE MEMBERS — `hosts/{hostId}/siteMembers`, newest first.\n *\n * The same fields and index shapes as the console's Site users list\n * (`apps/console/utils/site-account-query.ts`), so the composites are one\n * set. The search reads `searchTokens` — the start of any word of the\n * display name or of the address — which both writers of a member stamp\n * (`memberSearchTokens`, the commerce plugin's register and account routes)\n * and `backfill-site-member-search.mjs` stamps on the members written\n * before it. The register path lower-cases `email` before it stores it, so\n * the stored address is its own normalized key.\n *=========================================*/\nexport const SITE_MEMBER_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals'],\n },\n ],\n sorts: contactSorts('Joined'),\n search: { tokensPath: 'searchTokens' },\n}\n\nexport const SITE_MEMBER_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Email',\n}\n\n/*==========================================\n * LEADS — `orgs/{orgId}/leads`, newest first.\n *\n * Under a site the list is narrowed to what that site may see\n * (`visibleTo array-contains-any`, {@link leadListBase}); on the\n * organization's Inbox an org-wide member reads it unscoped.\n *\n * `searchTokens` — the lead's name, address, company, title and tags as word\n * prefixes — and `scopedSearchTokens` — the same behind each `visibleTo`\n * token — are the CRM's list fields, stamped on every lead write by\n * `crmLeadListFields` (`@aglyn/aglyn`'s CRM app-utils) and backfilled by the\n * CRM's own lane. Under a site the search folds into the scope clause\n * (`scopedSearch`), which the rules prove only for an org-wide member: a\n * collaborator whose reach is some sites keeps the scope clause, and their\n * search is asked as the start of the address beside it\n * (`leadListQueryFor`).\n *\n * Every current lead writer normalizes the address (`normalizeContactEmail`)\n * before `addHostLead` stores it, so Email is an equality on `email`.\n *=========================================*/\nconst LEAD_EMAIL_FIELD: ListFilterField = {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals'],\n}\n\nexport const LEAD_LIST_QUERY: ListQueryDeclaration = {\n fields: [LEAD_EMAIL_FIELD],\n sorts: contactSorts('Captured'),\n search: { tokensPath: 'searchTokens', scoped: scopedSearch('scopedSearchTokens') },\n}\n\n/**\n * The organization's Inbox adds what an unscoped read can ask: the surface\n * that captured the lead and the sites that did. Both are ARRAYS on the\n * lead (`sources`, `capturedByHostIds`, `arrayUnion`ed by `addHostLead`), so\n * each is the query's one array clause — \"any of\" — and cannot stand beside\n * the search or the other. Under a site the scope clause already holds that\n * one array clause, which is why neither is offered there.\n */\nexport const ORG_LEAD_LIST_QUERY: ListQueryDeclaration = {\n ...LEAD_LIST_QUERY,\n fields: [\n LEAD_EMAIL_FIELD,\n {\n column: 'sources',\n kind: 'exact',\n path: 'sources',\n tokensPath: 'sources',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n {\n column: 'capturedByHostIds',\n kind: 'exact',\n path: 'capturedByHostIds',\n tokensPath: 'capturedByHostIds',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n ],\n}\n\n/**\n * The column a site collaborator's search is asked through: the START of the\n * address, a prefix range on `email`. Not a Filters-panel field — the panel\n * reads the declaration the card shows, and this one rides only on the query.\n */\nexport const LEAD_ADDRESS_SEARCH_COLUMN = '$addressSearch'\n\nconst LEAD_ADDRESS_SEARCH_FIELD: ListFilterField = {\n column: LEAD_ADDRESS_SEARCH_COLUMN,\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n}\n\n/** The notice a site collaborator's search carries: what it matched, and the order. */\nexport const LEAD_ADDRESS_SEARCH_NOTICE =\n 'Search on this site’s leads matches the start of the address, and lists them by address.'\n\n/**\n * The query a reader may run for the Leads list, and whether its search was\n * asked as an address prefix (AGL-3321).\n *\n * An ORG-WIDE reader's search folds into the scope clause (`scopedSearch`):\n * a query without `visibleTo` is one the rules prove only for them. A reader\n * whose reach is some sites must keep `visibleTo array-contains-any` on the\n * query, which spends its one array clause — so their search is asked as a\n * prefix RANGE on the stored, normalized `email` beside the scope instead,\n * ordered by address (the `visibleTo, email` composite the lead list already\n * carries). Only the first word is asked.\n */\nexport function leadListQueryFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n request: ListQueryRequest,\n): { declaration: ListQueryDeclaration; request: ListQueryRequest; addressSearch: boolean } {\n if (foldsScope || !declaration.search?.scoped) return { declaration, request, addressSearch: false }\n const word = (request.search ?? []).join(' ').trim().split(/\\s+/)[0] ?? ''\n return {\n declaration: {\n ...declaration,\n fields: [...declaration.fields, LEAD_ADDRESS_SEARCH_FIELD],\n search: undefined,\n },\n request: {\n ...request,\n search: [],\n clauses: word\n ? [...request.clauses, { field: LEAD_ADDRESS_SEARCH_COLUMN, op: 'startsWith', value: word }]\n : request.clauses,\n },\n addressSearch: Boolean(word),\n }\n}\n\n/** The scope clause a site's lead list always carries, or none on the org's. */\nexport function leadListBase(scopeTokens: readonly string[] | null): ListQueryFilter[] {\n return scopeTokens ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...scopeTokens] }] : []\n}\n\n/** The base's shape for `listQueryIndexes`: the scope clause is an array clause. */\nexport const LEAD_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\nexport const LEAD_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Email',\n sources: 'Source',\n capturedByHostIds: 'Site',\n}\n\n/**\n * The capture surfaces a Source filter can name. A lead routed from a form\n * is filed as `form:{formId}` — one value per form, so no fixed choice can\n * ask for \"every form\" and none is offered; the rest are the fixed words the\n * doors write: a booking, a lead added by hand, over the API or from a file,\n * and `signup`, which member sign-ups wrote before they filed a contact.\n */\nexport const LEAD_SOURCE_OPTIONS: readonly ListFilterOption[] = [\n { value: 'booking', label: 'Booking' },\n { value: 'manual', label: 'Added by hand' },\n { value: 'api', label: 'API' },\n { value: 'import', label: 'Import' },\n { value: 'signup', label: 'Sign-up' },\n]\n\n/** How one capture surface reads on a row. */\nexport function leadSourceLabel(source: string): string {\n if (source === 'form' || source.startsWith('form:')) return 'Form'\n return LEAD_SOURCE_OPTIONS.find((option) => option.value === source)?.label ?? source\n}\n\n/*==========================================\n * FORM SUBMISSIONS — `hosts/{hostId}/formSubmissions` under a site, and the\n * `formSubmissions` collection group narrowed to `orgId` on the\n * organization's Inbox (the rules admit that group read only so narrowed).\n * Newest first.\n *\n * `senderTokens` and `searchTokens` are `messageSearchFields` of the\n * message, stamped by the submit route — the collection's only creator —\n * and by `backfill-form-submission-filters.mjs` on the rows before it; a\n * message's values never change after it arrives. `read` is a boolean on\n * every row: the route writes `false`, the reader and the API flip it, and\n * the same backfill stamps `false` on a row without one. `hostId` and\n * `orgId` are stamped by the route and frozen by the rules.\n *\n * ⛔ The form's NAME is not searched. The row keeps the name the form had\n * when the message arrived, and a rename would leave the search finding\n * rows by a name the reader no longer sees. Form is a filter by id instead,\n * which survives a rename.\n *=========================================*/\nconst SUBMISSION_FIELDS: readonly ListFilterField[] = [\n {\n column: 'from',\n kind: 'text',\n path: 'senderTokens',\n tokensPath: 'senderTokens',\n operators: ['contains'],\n },\n { column: 'read', kind: 'boolean', path: 'read', operators: ['equals'] },\n]\n\n/** Form: offered where a site's forms are read (not across every site). */\nconst FORM_FIELD: ListFilterField = {\n column: 'formId',\n kind: 'exact',\n path: 'formId',\n operators: ['equals'],\n}\n\n/** Site: the organization's Inbox only, where a row can be any site's. */\nconst SITE_FIELD: ListFilterField = {\n column: 'hostId',\n kind: 'exact',\n path: 'hostId',\n operators: ['equals', 'isAnyOf'],\n}\n\n/*\n * Received orders the query either way, and Read either way — unread first\n * (AGL-3680); both are on every row (see above). The headers past the\n * default are `alone`, served while nothing narrows the list past its scope.\n * From, Site and Message are drawn from the message, so they sort the page.\n */\nconst SUBMISSION_SORTS: ListQuerySort[] = [\n { path: 'createdAt', direction: 'desc', column: 'createdAt', label: 'Received' },\n { path: 'createdAt', direction: 'asc', column: 'createdAt', label: 'Received', alone: true },\n { path: 'read', direction: 'asc', column: 'read', label: 'Read', alone: true },\n { path: 'read', direction: 'desc', column: 'read', label: 'Read', alone: true },\n]\nconst SUBMISSION_SEARCH = { tokensPath: 'searchTokens' }\n\n/** A site's Inbox. */\nexport const SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: [...SUBMISSION_FIELDS, FORM_FIELD],\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** A card already narrowed to one form, where Form has nothing to pick. */\nexport const FORM_SCOPED_SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: SUBMISSION_FIELDS,\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** Every site's, on the organization's Inbox. */\nexport const ORG_SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: [...SUBMISSION_FIELDS, SITE_FIELD],\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** The organization's Inbox's scope, which the rules require. */\nexport const orgSubmissionBase = (orgId: string): ListQueryFilter[] => [\n { path: 'orgId', op: '==', value: orgId },\n]\n\n/** A form-scoped card's scope. */\nexport const formSubmissionBase = (formId: string): ListQueryFilter[] => [\n { path: 'formId', op: '==', value: formId },\n]\n\nexport const SUBMISSION_FILTER_HEADERS: Readonly<Record<string, string>> = {\n from: 'From',\n read: 'Read',\n formId: 'Form',\n hostId: 'Site',\n}\n\n/** Read is the stored boolean, by the words a reader uses for it. */\nexport const SUBMISSION_READ_OPTIONS: readonly ListFilterOption[] = [\n { value: 'false', label: 'Unread' },\n { value: 'true', label: 'Read' },\n]\n"],"names":["scopedSearch","contactSorts","createdLabel","path","direction","column","label","alone","SITE_MEMBER_LIST_QUERY","fields","kind","lowerPath","presence","operators","sorts","search","tokensPath","SITE_MEMBER_FILTER_HEADERS","email","LEAD_EMAIL_FIELD","LEAD_LIST_QUERY","scoped","ORG_LEAD_LIST_QUERY","verbatimTokens","LEAD_ADDRESS_SEARCH_COLUMN","LEAD_ADDRESS_SEARCH_FIELD","LEAD_ADDRESS_SEARCH_NOTICE","leadListQueryFor","declaration","foldsScope","request","addressSearch","word","join","trim","split","undefined","clauses","field","op","value","Boolean","leadListBase","scopeTokens","LEAD_LIST_BASE_INDEX","array","LEAD_FILTER_HEADERS","sources","capturedByHostIds","LEAD_SOURCE_OPTIONS","leadSourceLabel","source","startsWith","find","option","SUBMISSION_FIELDS","FORM_FIELD","SITE_FIELD","SUBMISSION_SORTS","SUBMISSION_SEARCH","SUBMISSION_LIST_QUERY","FORM_SCOPED_SUBMISSION_LIST_QUERY","ORG_SUBMISSION_LIST_QUERY","orgSubmissionBase","orgId","formSubmissionBase","formId","SUBMISSION_FILTER_HEADERS","from","read","hostId","SUBMISSION_READ_OPTIONS"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,YAAY,QAAQ,qCAAoC;AAUjE;;;;;;;;;CASC,GAED;;;;;;CAMC,GACD,MAAMC,eAAe,CAACC,eAA0C;QAC9D;YAAEC,MAAM;YAAaC,WAAW;YAAQC,QAAQ;YAAaC,OAAOJ;QAAa;QACjF;YAAEC,MAAM;YAAaC,WAAW;YAAOC,QAAQ;YAAaC,OAAOJ;YAAcK,OAAO;QAAK;QAC7F;YAAEJ,MAAM;YAASC,WAAW;YAAOC,QAAQ;YAASC,OAAO;YAASC,OAAO;QAAK;QAChF;YAAEJ,MAAM;YAASC,WAAW;YAAQC,QAAQ;YAASC,OAAO;YAASC,OAAO;QAAK;KAClF;AAED;;;;;;;;;;;2CAW2C,GAC3C,OAAO,MAAMC,yBAA+C;IAC1DC,QAAQ;QACN;YACEJ,QAAQ;YACRK,MAAM;YACNP,MAAM;YACNQ,WAAW;YACXC,UAAU;YACVC,WAAW;gBAAC;aAAS;QACvB;KACD;IACDC,OAAOb,aAAa;IACpBc,QAAQ;QAAEC,YAAY;IAAe;AACvC,EAAC;AAED,OAAO,MAAMC,6BAA+D;IAC1EC,OAAO;AACT,EAAC;AAED;;;;;;;;;;;;;;;;;;;2CAmB2C,GAC3C,MAAMC,mBAAoC;IACxCd,QAAQ;IACRK,MAAM;IACNP,MAAM;IACNQ,WAAW;IACXC,UAAU;IACVC,WAAW;QAAC;KAAS;AACvB;AAEA,OAAO,MAAMO,kBAAwC;IACnDX,QAAQ;QAACU;KAAiB;IAC1BL,OAAOb,aAAa;IACpBc,QAAQ;QAAEC,YAAY;QAAgBK,QAAQrB,aAAa;IAAsB;AACnF,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,MAAMsB,sBAA4C,aACpDF;IACHX,QAAQ;QACNU;QACA;YACEd,QAAQ;YACRK,MAAM;YACNP,MAAM;YACNa,YAAY;YACZO,gBAAgB;YAChBV,WAAW;gBAAC;aAAU;QACxB;QACA;YACER,QAAQ;YACRK,MAAM;YACNP,MAAM;YACNa,YAAY;YACZO,gBAAgB;YAChBV,WAAW;gBAAC;aAAU;QACxB;KACD;GACF;AAED;;;;CAIC,GACD,OAAO,MAAMW,6BAA6B,iBAAgB;AAE1D,MAAMC,4BAA6C;IACjDpB,QAAQmB;IACRd,MAAM;IACNP,MAAM;IACNQ,WAAW;IACXC,UAAU;IACVC,WAAW;QAAC;KAAa;AAC3B;AAEA,qFAAqF,GACrF,OAAO,MAAMa,6BACX,2FAA0F;AAE5F;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,iBACdC,WAAiC,EACjCC,UAAmB,EACnBC,OAAyB;QAGZ,mBAACA;QADKF;IAAnB,IAAIC,cAAc,GAACD,sBAAAA,YAAYb,MAAM,qBAAlBa,oBAAoBP,MAAM,GAAE,OAAO;QAAEO;QAAaE;QAASC,eAAe;IAAM;IACnG,MAAMC,QAAO,oBAAA,EAACF,kBAAAA,QAAQf,MAAM,YAAde,kBAAkB,EAAE,EAAEG,IAAI,CAAC,KAAKC,IAAI,GAAGC,KAAK,CAAC,MAAM,CAAC,EAAE,YAAvD,oBAA2D;IACxE,OAAO;QACLP,aAAa,aACRA;YACHnB,QAAQ;mBAAImB,YAAYnB,MAAM;gBAAEgB;aAA0B;YAC1DV,QAAQqB;;QAEVN,SAAS,aACJA;YACHf,QAAQ,EAAE;YACVsB,SAASL,OACL;mBAAIF,QAAQO,OAAO;gBAAE;oBAAEC,OAAOd;oBAA4Be,IAAI;oBAAcC,OAAOR;gBAAK;aAAE,GAC1FF,QAAQO,OAAO;;QAErBN,eAAeU,QAAQT;IACzB;AACF;AAEA,8EAA8E,GAC9E,OAAO,SAASU,aAAaC,WAAqC;IAChE,OAAOA,cAAc;QAAC;YAAExC,MAAM;YAAaoC,IAAI;YAAsBC,OAAO;mBAAIG;aAAY;QAAC;KAAE,GAAG,EAAE;AACtG;AAEA,kFAAkF,GAClF,OAAO,MAAMC,uBAAuB;IAAC;QAAEzC,MAAM;QAAa0C,OAAO;IAAK;CAAE,CAAS;AAEjF,OAAO,MAAMC,sBAAwD;IACnE5B,OAAO;IACP6B,SAAS;IACTC,mBAAmB;AACrB,EAAC;AAED;;;;;;CAMC,GACD,OAAO,MAAMC,sBAAmD;IAC9D;QAAET,OAAO;QAAWlC,OAAO;IAAU;IACrC;QAAEkC,OAAO;QAAUlC,OAAO;IAAgB;IAC1C;QAAEkC,OAAO;QAAOlC,OAAO;IAAM;IAC7B;QAAEkC,OAAO;QAAUlC,OAAO;IAAS;IACnC;QAAEkC,OAAO;QAAUlC,OAAO;IAAU;CACrC,CAAA;AAED,4CAA4C,GAC5C,OAAO,SAAS4C,gBAAgBC,MAAc;;QAErCF;IADP,IAAIE,WAAW,UAAUA,OAAOC,UAAU,CAAC,UAAU,OAAO;IAC5D,gBAAOH,4BAAAA,oBAAoBI,IAAI,CAAC,CAACC,SAAWA,OAAOd,KAAK,KAAKW,4BAAtDF,0BAA+D3C,KAAK,mBAAI6C;AACjF;AAEA;;;;;;;;;;;;;;;;;;2CAkB2C,GAC3C,MAAMI,oBAAgD;IACpD;QACElD,QAAQ;QACRK,MAAM;QACNP,MAAM;QACNa,YAAY;QACZH,WAAW;YAAC;SAAW;IACzB;IACA;QAAER,QAAQ;QAAQK,MAAM;QAAWP,MAAM;QAAQU,WAAW;YAAC;SAAS;IAAC;CACxE;AAED,yEAAyE,GACzE,MAAM2C,aAA8B;IAClCnD,QAAQ;IACRK,MAAM;IACNP,MAAM;IACNU,WAAW;QAAC;KAAS;AACvB;AAEA,wEAAwE,GACxE,MAAM4C,aAA8B;IAClCpD,QAAQ;IACRK,MAAM;IACNP,MAAM;IACNU,WAAW;QAAC;QAAU;KAAU;AAClC;AAEA;;;;;CAKC,GACD,MAAM6C,mBAAoC;IACxC;QAAEvD,MAAM;QAAaC,WAAW;QAAQC,QAAQ;QAAaC,OAAO;IAAW;IAC/E;QAAEH,MAAM;QAAaC,WAAW;QAAOC,QAAQ;QAAaC,OAAO;QAAYC,OAAO;IAAK;IAC3F;QAAEJ,MAAM;QAAQC,WAAW;QAAOC,QAAQ;QAAQC,OAAO;QAAQC,OAAO;IAAK;IAC7E;QAAEJ,MAAM;QAAQC,WAAW;QAAQC,QAAQ;QAAQC,OAAO;QAAQC,OAAO;IAAK;CAC/E;AACD,MAAMoD,oBAAoB;IAAE3C,YAAY;AAAe;AAEvD,oBAAoB,GACpB,OAAO,MAAM4C,wBAA8C;IACzDnD,QAAQ;WAAI8C;QAAmBC;KAAW;IAC1C1C,OAAO4C;IACP3C,QAAQ4C;AACV,EAAC;AAED,yEAAyE,GACzE,OAAO,MAAME,oCAA0D;IACrEpD,QAAQ8C;IACRzC,OAAO4C;IACP3C,QAAQ4C;AACV,EAAC;AAED,+CAA+C,GAC/C,OAAO,MAAMG,4BAAkD;IAC7DrD,QAAQ;WAAI8C;QAAmBE;KAAW;IAC1C3C,OAAO4C;IACP3C,QAAQ4C;AACV,EAAC;AAED,+DAA+D,GAC/D,OAAO,MAAMI,oBAAoB,CAACC,QAAqC;QACrE;YAAE7D,MAAM;YAASoC,IAAI;YAAMC,OAAOwB;QAAM;KACzC,CAAA;AAED,gCAAgC,GAChC,OAAO,MAAMC,qBAAqB,CAACC,SAAsC;QACvE;YAAE/D,MAAM;YAAUoC,IAAI;YAAMC,OAAO0B;QAAO;KAC3C,CAAA;AAED,OAAO,MAAMC,4BAA8D;IACzEC,MAAM;IACNC,MAAM;IACNH,QAAQ;IACRI,QAAQ;AACV,EAAC;AAED,mEAAmE,GACnE,OAAO,MAAMC,0BAAuD;IAClE;QAAE/B,OAAO;QAASlC,OAAO;IAAS;IAClC;QAAEkC,OAAO;QAAQlC,OAAO;IAAO;CAChC,CAAA"}
|