@aglyn/plugins-forms 1.0.0-beta.198 → 1.0.0-beta.200
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 +12 -12
- package/src/lib/components/form-detail-card.js +6 -2
- package/src/lib/components/form-detail-card.js.map +1 -1
- package/src/lib/components/form-list-query.d.ts +48 -0
- package/src/lib/components/form-list-query.js +216 -0
- package/src/lib/components/form-list-query.js.map +1 -0
- package/src/lib/components/host-forms-card.component.js +174 -108
- package/src/lib/components/host-forms-card.component.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-forms",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.200",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.200",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.200",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.200",
|
|
31
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.200",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.200",
|
|
33
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.200",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.200",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.200",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.200",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.200",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.200",
|
|
39
39
|
"@swc/helpers": "0.5.23"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -226,9 +226,13 @@ import useFormPromoteApi from "./use-form-promote-api.js";
|
|
|
226
226
|
const dequeue = queueLoading();
|
|
227
227
|
try {
|
|
228
228
|
var _ref;
|
|
229
|
-
await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), _extends({}, name != null ? {
|
|
229
|
+
await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), _extends({}, name != null ? _extends({
|
|
230
230
|
displayName: name.trim()
|
|
231
|
-
}
|
|
231
|
+
}, Aglyn.formListFields({
|
|
232
|
+
id: formId,
|
|
233
|
+
displayName: name.trim(),
|
|
234
|
+
slug: form == null ? void 0 : form.slug
|
|
235
|
+
})) : {}, lead != null ? {
|
|
232
236
|
routing: _extends({}, (_ref = form == null ? void 0 : form.routing) != null ? _ref : {}, {
|
|
233
237
|
lead
|
|
234
238
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-detail-card.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 * as Aglyn from '@aglyn/aglyn'\nimport {\n buildRoute,\n type FormFieldDecl,\n formFieldsCanYieldAnEmail,\n formFieldsCaptureConsent,\n PageHeaderActions,\n PageHeaderRecord,\n listConsoleWidgets,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n// The CRM's route builder by its leaf path, not the plugin barrel: the barrel\n// is the entry point the tenant's loader imports to activate the plugin's\n// site half, and a console page named there would ship to every published\n// page. The same import the Inbox makes for the same reason.\nimport { pluginRecordFilteredHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { ICON_VARIANT_BESIGNER } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay, GridItems, MdiIcon, useLoading } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Alert,\n AlertTitle,\n Button,\n Chip,\n FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useHostCampaigns,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { collection, doc, limit, query, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { FORM_CONTACT_FIELDS_ZONE } from './form-zones'\nimport FormDesignPreview from './form-design-preview.component'\nimport FormMetricsCard from './form-metrics-card.component'\nimport FormSubmissionsCard from './form-submissions-card.component'\nimport useFormPromoteApi from './use-form-promote-api'\n\nexport interface FormDetailCardProps {\n hostId: string\n formId: string\n /**\n * The Forms surface's own absolute console path.\n *\n * Only the not-found branch links it. The trail carries the way back on a\n * form that exists, so a link beside the heading would be the breadcrumb\n * written twice; a form that does NOT exist has no heading of its own for\n * the trail to end on, and the reader needs somewhere to go.\n */\n basePath?: string\n /** Whether this viewer's role on the site may make a version live. */\n canPublish?: boolean\n /** False while the role is still being read; see the disabled reasons. */\n hostRoleLoaded?: boolean\n}\n\n/**\n * One form — the component detail surface, for a document that is also a\n * contract.\n *\n * The split this enforces is the reason it exists rather than sending a row\n * straight into the besigner. A form has two halves:\n *\n * - the DESIGN, which is what an author draws, and which lives in the\n * besigner reached from here;\n * - the DECLARATION — where a submission is routed and which field carries\n * marketing consent — which is edited here.\n *\n * They are edited apart because the declaration is what the design is then\n * checked AGAINST. `checkFormContract` compares the two at publish, so a\n * surface that let an author change both in one motion would let them satisfy\n * the check by moving whichever side happened to be easier — which is how a\n * form ends up with lead routing pointed at a field nobody fills in.\n *\n * ## Promotion lives here, not only in the besigner\n *\n * A component's version history offers Publish on any version: promotion is\n * how you go back, not only how you go forward, and an author restoring last\n * week's design should not have to open a canvas to do it.\n *\n * It rides `/api/hosts/forms/promote` rather than an `updateDoc` here, because\n * a form's promotion has to run `checkFormContract` on the tree it is about to\n * write and REFUSE. A check in this component would be advice a determined\n * client could skip; the route reads the stored version itself, so nothing\n * about the design crosses the wire inbound and there is no version of this\n * surface that can publish a design the check has not seen.\n */\nexport function FormDetailCard(props: FormDetailCardProps) {\n const { hostId, formId, basePath, canPublish, hostRoleLoaded } = props\n const firestore = useFirestore()\n const ContactFieldsZone = useConsoleWidgetSlot()\n // Read at render, after the shell has loaded its plugins.\n const hasContactFieldsWidget =\n listConsoleWidgets(FORM_CONTACT_FIELDS_ZONE.id).length > 0\n /**\n * The write a widget in the contact-fields zone asks for. The declaration\n * is one array on the form document, and the document is this plugin's, so\n * the page writes what the widget decided.\n */\n const saveFields = useCallback(\n async (fields: FormFieldDecl[]) => {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n fields,\n updatedAt: Timestamp.now(),\n })\n },\n [firestore, hostId, formId],\n )\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const createHostVersion = useHostVersionApi()\n const promoteForm = useFormPromoteApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { queueLoading } = useLoading()\n\n const { data: form, status } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'forms', formId),\n [firestore, hostId, formId],\n { idField: '$id' },\n )\n // Three states, not two (AGL-706): loading and absent both arrive as\n // `undefined`, and an editable form over a mistyped id reads as data loss.\n const notFound = status !== 'loading' && !form\n\n /**\n * The published design, in BOTH stored forms (AGL-1151).\n *\n * The read above is converter-less, so `nodes` arrives exactly as stored —\n * msgpack for a design promoted since forms were compressed, a plain map\n * for everything older. Decoded once here because two surfaces below need\n * it and both fail silently on the raw value: `checkFormContract` reports\n * bogus violations against a tree it cannot walk, and the AGL-753 empty\n * guard passes on a `Bytes` because `Object.keys` counts the wrapper.\n */\n const formNodes = useMemo(\n () => Aglyn.decodeStoredNodes<Record<string, any>>(form?.nodes),\n [form?.nodes],\n )\n\n // No orderBy: the oldest version docs predate `createdAt`, and Firestore\n // drops documents missing the ordered field.\n const { data: versionDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms', formId, 'versions'),\n limit(100),\n ),\n [firestore, hostId, formId],\n { idField: '$id' },\n )\n const versions = [...(versionDocs ?? [])].sort(\n (a: any, b: any) =>\n (b.createdAt?.seconds ?? 0) - (a.createdAt?.seconds ?? 0),\n )\n const publishedVersionId = form?.versionId as string | undefined\n\n const [name, setName] = useState<string | null>(null)\n const [lead, setLead] = useState<boolean | null>(null)\n const [consentField, setConsentField] = useState<string | null>(null)\n /**\n * The campaigns picked on screen, or null while the stored value stands.\n *\n * Null and `[]` are different: null means \"not touched\", `[]` means \"take\n * this form out of every campaign\". Collapsing them would make clearing the\n * last campaign a no-op, which is the half of set-and-clear that is easy to\n * ship broken.\n */\n const [campaignIds, setCampaignIds] = useState<string[] | null>(null)\n const [opening, setOpening] = useState(false)\n const [promoting, setPromoting] = useState<string | null>(null)\n /**\n * What a REFUSED promotion said, kept on the surface rather than in a toast.\n *\n * A contract violation is a list of things to go and fix in the besigner,\n * and a snackbar is gone by the time the author has read the second one.\n * The besigner's own refusal persists for the same reason.\n */\n const [promoteRefusal, setPromoteRefusal] = useState<{\n message: string\n violations: Aglyn.FormContractViolation[]\n } | null>(null)\n\n /** The fields the PUBLISHED design draws, which is what a submission can carry. */\n const declaredFields: Aglyn.FormFieldDecl[] = useMemo(\n () => (Array.isArray(form?.fields) ? form.fields : []),\n [form],\n )\n\n const effectiveLead = lead ?? form?.routing?.lead === true\n const effectiveConsent = consentField ?? String(form?.consentFieldName ?? '')\n /*\n * Whether lead routing COULD work on this design — the two preconditions\n * `checkFormContract` applies at publish, asked here so the switch says so\n * before it is flipped rather than after (AGL-2612). Consent is read as\n * it is being edited: picking a field below is what satisfies it.\n */\n const hasEmailField = formFieldsCanYieldAnEmail(declaredFields)\n const capturesConsent = formFieldsCaptureConsent(declaredFields, effectiveConsent)\n /*\n * Where this form's people are: the contacts list narrowed to this form,\n * asked of the plugin that keeps people (`null`, and no link, without one). Built from the resolved org slug and\n * subdomain the besigner link already waits on, so the link appears when\n * they land and never points at a half-built address.\n */\n const contactsHref =\n orgSlug && host\n ? pluginRecordFilteredHref('contact', { orgSlug, host }, 'form', formId)\n : null\n /*\n * WHICH CAMPAIGNS THIS FORM IS PART OF.\n *\n * The site's campaigns are read while this page is open because the field\n * is DISPLAYED here as well as edited here: the document stores ids, and a\n * page that drew them until a button was pressed would be showing a\n * merchant raw storage. Bounded at the ceiling the campaigns table itself\n * draws, so it can offer nothing that list does not.\n */\n const siteCampaigns = useHostCampaigns(hostId, { enabled: Boolean(form) })\n const storedCampaigns = useMemo(\n () => Aglyn.readCampaignIds(form as Record<string, unknown>),\n [form],\n )\n const effectiveCampaigns = campaignIds ?? storedCampaigns\n\n /**\n * What the published design would break, judged against the declaration as\n * it is being edited here.\n *\n * Shown here as well as at publish because the two halves are edited in\n * different places: turning lead routing on for a form whose design has no\n * email field is a break the author causes HERE, on a design they are not\n * looking at, and the besigner would not tell them until they next opened\n * it.\n */\n const violations = useMemo(\n () =>\n form\n ? Aglyn.checkFormContract({\n form: {\n routing: { ...(form.routing ?? {}), lead: effectiveLead },\n ...(effectiveConsent\n ? { consentFieldName: effectiveConsent }\n : {}),\n },\n formId,\n nodes: formNodes as never,\n formNodeId: findFormNodeId(formNodes as never),\n })\n : [],\n [form, formId, effectiveLead, effectiveConsent],\n )\n\n const handleSave = useCallback(async () => {\n const dequeue = queueLoading()\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n ...(name != null ? { displayName: name.trim() } : {}),\n ...(lead != null ? { routing: { ...(form?.routing ?? {}), lead } } : {}),\n ...(consentField != null\n ? { consentFieldName: consentField.trim() }\n : {}),\n // An empty selection is stored as an empty array rather than removing\n // the field: one shape for \"in no campaign\", which is what keeps this\n // writer and the campaign's own detach agreeing.\n ...(campaignIds != null\n ? {\n [Aglyn.CAMPAIGN_MEMBERSHIP_FIELD]:\n Aglyn.campaignMembershipValue(campaignIds),\n }\n : {}),\n updatedAt: Timestamp.now(),\n })\n setName(null)\n setLead(null)\n setConsentField(null)\n setCampaignIds(null)\n enqueueSnackbar('Form saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n }, [\n firestore,\n hostId,\n formId,\n form,\n name,\n lead,\n consentField,\n campaignIds,\n queueLoading,\n enqueueSnackbar,\n ])\n\n /**\n * Makes one version the version the live sites serve.\n *\n * The route decides, not this handler: it re-reads the stored version, runs\n * `checkFormContract` on the tree it is about to write, and answers 422 with\n * the violations when a publish would stop the submissions arriving. So the\n * only thing here is what to do with each answer — and a refusal is put on\n * the page rather than in a toast, because it is a list of things to fix.\n */\n const handlePromote = useCallback(\n (targetVersionId: string) => async () => {\n if (promoting) return\n setPromoting(targetVersionId)\n setPromoteRefusal(null)\n try {\n const result = await promoteForm({ hostId, formId, versionId: targetVersionId })\n if (!result.ok) {\n const message = result.message ?? 'Publish failed'\n setPromoteRefusal({\n message,\n violations: result.violations ?? [],\n })\n // `persist` because this is a refusal an author has to act on: an\n // auto-dismissed warning is how someone walks away believing the\n // form shipped.\n enqueueSnackbar(message, {\n variant: 'warning',\n allowDuplicate: true,\n persist: true,\n })\n return\n }\n enqueueSnackbar(\n 'Published. The live sites now serve this version, and the form’s ' +\n 'declared fields match it.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Could not publish that version', {\n variant: 'error',\n allowDuplicate: true,\n persist: true,\n })\n } finally {\n setPromoting(null)\n }\n },\n [promoting, promoteForm, hostId, formId, enqueueSnackbar],\n )\n\n /**\n * Opens a version in the besigner, minting the first one when the form has\n * none.\n *\n * Forms adopted from a page carry a declared field list and a `nodes` map\n * seeded from the page they were adopted from — so the first version opens\n * on the form the site was ALREADY submitting rather than a blank canvas.\n */\n const handleOpen = useCallback(\n (targetVersionId?: string) => async () => {\n if (opening) return\n setOpening(true)\n try {\n let versionId = targetVersionId ?? publishedVersionId ?? versions[0]?.$id\n if (!versionId) {\n versionId = Aglyn.createResourceUid()\n // Rides /api/hosts/versions: rules deny the client create, and the\n // route allows a resource's FIRST version on every plan — which\n // this always is, since the form reached here with no `versionId`.\n await createHostVersion({\n hostId,\n kind: 'form',\n parentId: formId,\n id: versionId,\n data: {\n formId,\n hostId,\n displayName: 'Initial version',\n // Emptiness, not just absence: a form that holds no `nodes` and\n // `??` would pass an empty map straight through to a version\n // that opens uneditable (AGL-753). Measured on the DECODED\n // tree — `Object.keys` over a stored `Bytes` is non-empty, so\n // the raw value passes this guard and mints exactly the\n // uneditable version it exists to stop (AGL-1151).\n nodes: Object.keys(formNodes ?? {}).length\n ? formNodes\n : {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: [],\n },\n },\n ...(form?.rootId ? { rootId: form.rootId } : {}),\n },\n })\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n versionId,\n updatedAt: Timestamp.now(),\n })\n }\n // The editor's URL needs the resolved org slug and subdomain, which\n // `useConsoleHostRoute` reads. Until they land the button below is\n // disabled, so this branch cannot be reached without them.\n if (orgSlug && host) {\n router.push(\n buildRoute(Route.FORM_BESIGNER, { orgSlug, host, formId, versionId }),\n )\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Could not open the besigner', {\n variant: 'error',\n allowDuplicate: true,\n })\n setOpening(false)\n }\n },\n [\n opening,\n createHostVersion,\n publishedVersionId,\n versions,\n firestore,\n hostId,\n formId,\n form,\n router,\n orgSlug,\n host,\n enqueueSnackbar,\n ],\n )\n\n const dirty =\n name != null ||\n lead != null ||\n consentField != null ||\n (campaignIds != null &&\n !Aglyn.campaignMembershipUnchanged(storedCampaigns, campaignIds))\n // Named before it is used on every row, so a role denial reads as a reason\n // rather than as a control that does nothing.\n const publishBlock = hostRoleLoaded\n ? 'Your role on this site can edit content but not publish it'\n : 'Checking what your role can do…'\n\n if (notFound) {\n return (\n <CardDisplay\n header=\"Form not found\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'A form is created from the Forms list and keeps the id every ' +\n 'submission is filed under.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2} sx={{ alignItems: 'flex-start' }}>\n <Alert severity=\"info\">\n {`No form on this site has the id ${formId}. It may have been deleted, or the link may be from another site.`}\n </Alert>\n {basePath ? <AppLink href={basePath}>{'Back to forms'}</AppLink> : null}\n </Stack>\n </CardDisplay>\n )\n }\n\n /* The cards, named so the page chrome above them is a plain list of\n what this surface publishes upward. */\n const cards = (\n <GridItems\n spacing={3}\n items={[\n {\n size: { xs: 12, lg: 5 },\n children: (\n <CardDisplay\n header=\"Details\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'The id is what every submission is filed under, so ' +\n 'renaming a form never splits its history.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <TextField\n label=\"Display name\"\n size=\"small\"\n value={name ?? form?.displayName ?? ''}\n onChange={(event) => setName(event.target.value)}\n fullWidth\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`ID ${formId} — every submission is filed under this, so it never changes`}\n </Typography>\n {/*\n A FORM IS PART OF A CAMPAIGN, and may be part of several.\n The same signup form is placed by one push and re-placed by\n the next, so this is a multi-select — a single value would\n make filing it under this quarter's campaign silently\n un-file it from last quarter's.\n\n It is an assignment and nothing else: it does not decide who\n a campaign mails, and it does not credit a submission to\n one. Attribution is recorded from the link a visitor\n followed, on its own evidence, and it goes on saying what it\n says whatever is picked here.\n */}\n <CampaignPicker\n options={siteCampaigns.options}\n value={effectiveCampaigns}\n onChange={setCampaignIds}\n helperText=\"The campaigns this form is part of. It does not change who a campaign mails.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n />\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={!dirty}\n onClick={handleSave}\n >\n {'Save'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n ),\n },\n {\n size: { xs: 12, lg: 7 },\n children: (\n <CardDisplay\n header=\"CRM routing\"\n help={pluginDocsHelp('forms', {\n anchor: '#where-submissions-go',\n excerpt:\n 'Every submission reaches the Inbox and updates the ' +\n 'contact. Lead routing and the consent field decide ' +\n 'what else happens to it.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {/*\n THE PART THAT IS NOT A SWITCH (AGL-2612). Every capture\n lands in Contacts whatever is set below; the switch decides\n only whether a LEAD is filed beside the contact. Said\n first, because the switch on its own read as the only way\n a submission reached the CRM.\n */}\n <Typography variant=\"body2\">\n {'Every submission with an email address updates the person ' +\n 'in Contacts at stage Lead — a new contact when the ' +\n 'address is new, an interaction on their timeline when it ' +\n 'is not. A customer stays a customer.'}\n </Typography>\n <Stack spacing={0.5}>\n <FormControlLabel\n control={\n <Switch\n checked={effectiveLead}\n onChange={(event) => setLead(event.target.checked)}\n />\n }\n label=\"Also create a lead from the address someone gives this form\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {!hasEmailField\n ? 'Needs an email field in the published design — a ' +\n 'submission without an address cannot key a lead, and ' +\n 'publishing with this on would be refused until one exists.'\n : !capturesConsent\n ? 'Needs a marketing consent field — a lead nobody opted ' +\n 'in with is one the team cannot email, and publishing ' +\n 'with this on would be refused until the field below ' +\n 'names one.'\n : 'Files a lead in CRM → Leads for the sales team to work, ' +\n 'carrying the opt-in the consent field records.'}\n </Typography>\n </Stack>\n <TextField\n select\n label=\"Marketing consent field\"\n size=\"small\"\n value={effectiveConsent}\n onChange={(event) => setConsentField(event.target.value)}\n fullWidth\n helperText=\"The one field that IS the opt-in. Submitting a form is never itself consent.\"\n >\n <MenuItem value=\"\">\n {'None — this form collects no opt-in'}\n </MenuItem>\n {declaredFields.map((field) => (\n <MenuItem key={field.fieldName} value={field.fieldName}>\n {field.label\n ? `${field.label} (${field.fieldName})`\n : field.fieldName}\n </MenuItem>\n ))}\n </TextField>\n {violations.length ? (\n <Alert severity=\"warning\">\n <AlertTitle>{'This form would stop collecting'}</AlertTitle>\n <Stack component=\"ul\" sx={{ pl: 2, m: 0 }}>\n {violations.map((violation) => (\n <Typography\n component=\"li\"\n variant=\"body2\"\n key={`${violation.code}:${violation.nodeId ?? violation.fieldName ?? ''}`}\n >\n {violation.message}\n </Typography>\n ))}\n </Stack>\n </Alert>\n ) : null}\n {/*\n The people this form captured, on the list that holds them.\n Withheld until the site's address resolves, for the reason\n the besigner button is.\n */}\n {contactsHref ? (\n <Typography variant=\"body2\">\n <AppLink href={contactsHref}>\n {'See the contacts this form captured in the CRM'}\n </AppLink>\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n ),\n },\n // Where each field saves on the person: another plugin's card, in a\n // zone this page hosts. No item at all where nothing fills it, so the\n // grid keeps no empty cell.\n ...(ContactFieldsZone && hasContactFieldsWidget\n ? [\n {\n size: { xs: 12 },\n children: (\n <ContactFieldsZone\n slot={FORM_CONTACT_FIELDS_ZONE.id}\n hostId={hostId}\n formId={formId}\n fields={declaredFields}\n loading={status === 'loading'}\n saveFields={saveFields}\n />\n ),\n },\n ]\n : []),\n {\n size: { xs: 12, lg: 5 },\n children: (\n <FormMetricsCard\n stats={form?.stats}\n fields={declaredFields}\n leadRouting={effectiveLead}\n loading={status === 'loading'}\n />\n ),\n },\n {\n size: { xs: 12, lg: 7 },\n children: (\n <CardDisplay\n header=\"Versions\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'Publishing a form version updates every page that ' +\n 'places it. Saving does not — a save is not a publish.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {promoteRefusal ? (\n <Alert\n severity=\"warning\"\n onClose={() => setPromoteRefusal(null)}\n >\n <AlertTitle>{promoteRefusal.message}</AlertTitle>\n {promoteRefusal.violations.length ? (\n <Stack component=\"ul\" sx={{ pl: 2, m: 0 }}>\n {promoteRefusal.violations.map((violation) => (\n <Typography\n component=\"li\"\n variant=\"body2\"\n key={`${violation.code}:${violation.nodeId ?? violation.fieldName ?? ''}`}\n >\n {violation.message}\n </Typography>\n ))}\n </Stack>\n ) : null}\n </Alert>\n ) : null}\n {versions.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No versions yet — opening the besigner creates the first one.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Version'}</TableCell>\n <TableCell>{'Created'}</TableCell>\n <TableCell>{'Updated'}</TableCell>\n <TableCell align=\"right\">{'Actions'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {versions.map((version: any) => (\n <TableRow key={version.$id} hover>\n <TableCell>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"body2\">\n {version.displayName ?? version.$id}\n </Typography>\n {version.$id === publishedVersionId ? (\n <Chip\n label=\"Current\"\n color=\"success\"\n size=\"small\"\n />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n {version.createdAt?.toDate?.().toLocaleString() ??\n '--'}\n </TableCell>\n <TableCell>\n {version.updatedAt?.toDate?.().toLocaleString() ??\n '--'}\n </TableCell>\n <TableCell align=\"right\">\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n <Button\n size=\"small\"\n disabled={opening || !orgSlug || !host}\n onClick={handleOpen(version.$id)}\n >\n {'Open'}\n </Button>\n {/*\n The version the sites already serve has nothing\n to promote to, so the control is disabled\n rather than absent — an absent control and an\n inapplicable one look identical, and only one\n of them is honest.\n */}\n <Button\n size=\"small\"\n variant=\"outlined\"\n title={canPublish ? undefined : publishBlock}\n disabled={\n !canPublish ||\n promoting !== null ||\n version.$id === publishedVersionId\n }\n onClick={handlePromote(version.$id)}\n >\n {promoting === version.$id\n ? 'Publishing…'\n : version.$id === publishedVersionId\n ? 'Published'\n : 'Publish'}\n </Button>\n </Stack>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n )}\n </Stack>\n </CardDisplay>\n ),\n },\n {\n /*\n THE MESSAGES THEMSELVES, under the numbers that describe them.\n Full width because it is a table of people and what they wrote,\n and it sits below the metrics because those are read at a glance\n and this is read a row at a time.\n */\n size: { xs: 12 },\n children: <FormSubmissionsCard hostId={hostId} formId={formId} />,\n },\n {\n // Full width: the frame is a document, and a document in a\n // five-column card is a column of one-word lines.\n size: { xs: 12 },\n children: (\n <CardDisplay\n header=\"What a submission will carry\"\n help={pluginDocsHelp('forms', {\n anchor: '#field-types',\n excerpt:\n 'Each field arrives under its own name. Two fields ' +\n 'sharing one name arrive as a single answer.',\n })}\n contentGutterX\n contentGutterY\n >\n <FormDesignPreview\n formId={formId}\n nodes={form?.nodes}\n consentFieldName={\n effectiveConsent ? effectiveConsent : undefined\n }\n loading={status === 'loading'}\n />\n </CardDisplay>\n ),\n },\n ]}\n />\n )\n\n return (\n <>\n {/*\n The page chrome this surface cannot set for itself. The console shell\n owns the layout and builds its heading from the Forms nav item, so\n without these the page about one form is headed `Forms` and its trail\n stops on the list — the same on every row of it.\n */}\n <PageHeaderRecord\n title={form ? form.displayName || formId : undefined}\n />\n {/*\n The besigner is what this surface exists to reach, so it leads — in\n the PAGE header, which is where an action belongs on a surface with\n no section rail beneath it. Withheld when there is no form: it would\n mint a version document under an id that has none (AGL-706).\n */}\n <PageHeaderActions>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={opening || !orgSlug || !host}\n title={\n orgSlug && host ? undefined : 'Resolving this site’s address…'\n }\n onClick={handleOpen()}\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {opening ? 'Opening…' : 'Edit in besigner'}\n </Button>\n </PageHeaderActions>\n {cards}\n </>\n )\n}\nFormDetailCard.displayName = 'FormDetailCard'\n\n/**\n * The `form` node inside a published design.\n *\n * A form document's tree holds exactly one, because the document IS that\n * form — but the tree is a flat map with a synthetic root, so the node has to\n * be found rather than assumed to be the root itself.\n */\nfunction findFormNodeId(\n nodes: Record<string, { componentId?: string }> | undefined,\n): string | undefined {\n return Object.keys(nodes ?? {}).find(\n (id) => nodes?.[id]?.componentId === 'form',\n )\n}\n\nexport default FormDetailCard\n"],"names":["Aglyn","buildRoute","formFieldsCanYieldAnEmail","formFieldsCaptureConsent","PageHeaderActions","PageHeaderRecord","listConsoleWidgets","pluginDocsHelp","Route","useConsoleWidgetSlot","pluginRecordFilteredHref","ICON_VARIANT_BESIGNER","AppLink","CardDisplay","GridItems","MdiIcon","useLoading","ScrollTable","useSnackbar","Timestamp","Alert","AlertTitle","Button","Chip","FormControlLabel","MenuItem","Stack","Switch","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useConsoleHostRoute","useFirestore","useFirestoreCollection","useFirestoreDoc","useHostCampaigns","useHostVersionApi","CampaignPicker","collection","doc","limit","query","updateDoc","useRouter","useCallback","useMemo","useState","FORM_CONTACT_FIELDS_ZONE","FormDesignPreview","FormMetricsCard","FormSubmissionsCard","useFormPromoteApi","FormDetailCard","props","name","form","hostId","formId","basePath","canPublish","hostRoleLoaded","firestore","ContactFieldsZone","hasContactFieldsWidget","id","length","saveFields","fields","updatedAt","now","orgSlug","subdomain","host","createHostVersion","promoteForm","router","enqueueSnackbar","queueLoading","data","status","idField","notFound","formNodes","decodeStoredNodes","nodes","versionDocs","versions","sort","a","b","createdAt","seconds","publishedVersionId","versionId","setName","lead","setLead","consentField","setConsentField","campaignIds","setCampaignIds","opening","setOpening","promoting","setPromoting","promoteRefusal","setPromoteRefusal","declaredFields","Array","isArray","effectiveLead","routing","effectiveConsent","String","consentFieldName","hasEmailField","capturesConsent","contactsHref","siteCampaigns","enabled","Boolean","storedCampaigns","readCampaignIds","effectiveCampaigns","violations","checkFormContract","formNodeId","findFormNodeId","handleSave","dequeue","displayName","trim","CAMPAIGN_MEMBERSHIP_FIELD","campaignMembershipValue","variant","persist","error","console","allowDuplicate","handlePromote","targetVersionId","result","ok","message","handleOpen","$id","createResourceUid","kind","parentId","Object","keys","CANVAS_ROOT_ELEMENT_ID","componentId","rootId","push","FORM_BESIGNER","dirty","campaignMembershipUnchanged","publishBlock","header","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","sx","alignItems","severity","href","cards","items","size","xs","lg","children","label","value","onChange","event","target","fullWidth","color","options","helperText","empty","ready","direction","disabled","onClick","control","checked","select","map","field","fieldName","component","pl","m","violation","code","nodeId","slot","loading","stats","leadRouting","onClose","align","version","hover","toDate","toLocaleString","justifyContent","title","undefined","startIcon","path","find"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-detail-card.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 * as Aglyn from '@aglyn/aglyn'\nimport {\n buildRoute,\n type FormFieldDecl,\n formFieldsCanYieldAnEmail,\n formFieldsCaptureConsent,\n PageHeaderActions,\n PageHeaderRecord,\n listConsoleWidgets,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n// The CRM's route builder by its leaf path, not the plugin barrel: the barrel\n// is the entry point the tenant's loader imports to activate the plugin's\n// site half, and a console page named there would ship to every published\n// page. The same import the Inbox makes for the same reason.\nimport { pluginRecordFilteredHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { ICON_VARIANT_BESIGNER } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay, GridItems, MdiIcon, useLoading } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Alert,\n AlertTitle,\n Button,\n Chip,\n FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useHostCampaigns,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { collection, doc, limit, query, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { FORM_CONTACT_FIELDS_ZONE } from './form-zones'\nimport FormDesignPreview from './form-design-preview.component'\nimport FormMetricsCard from './form-metrics-card.component'\nimport FormSubmissionsCard from './form-submissions-card.component'\nimport useFormPromoteApi from './use-form-promote-api'\n\nexport interface FormDetailCardProps {\n hostId: string\n formId: string\n /**\n * The Forms surface's own absolute console path.\n *\n * Only the not-found branch links it. The trail carries the way back on a\n * form that exists, so a link beside the heading would be the breadcrumb\n * written twice; a form that does NOT exist has no heading of its own for\n * the trail to end on, and the reader needs somewhere to go.\n */\n basePath?: string\n /** Whether this viewer's role on the site may make a version live. */\n canPublish?: boolean\n /** False while the role is still being read; see the disabled reasons. */\n hostRoleLoaded?: boolean\n}\n\n/**\n * One form — the component detail surface, for a document that is also a\n * contract.\n *\n * The split this enforces is the reason it exists rather than sending a row\n * straight into the besigner. A form has two halves:\n *\n * - the DESIGN, which is what an author draws, and which lives in the\n * besigner reached from here;\n * - the DECLARATION — where a submission is routed and which field carries\n * marketing consent — which is edited here.\n *\n * They are edited apart because the declaration is what the design is then\n * checked AGAINST. `checkFormContract` compares the two at publish, so a\n * surface that let an author change both in one motion would let them satisfy\n * the check by moving whichever side happened to be easier — which is how a\n * form ends up with lead routing pointed at a field nobody fills in.\n *\n * ## Promotion lives here, not only in the besigner\n *\n * A component's version history offers Publish on any version: promotion is\n * how you go back, not only how you go forward, and an author restoring last\n * week's design should not have to open a canvas to do it.\n *\n * It rides `/api/hosts/forms/promote` rather than an `updateDoc` here, because\n * a form's promotion has to run `checkFormContract` on the tree it is about to\n * write and REFUSE. A check in this component would be advice a determined\n * client could skip; the route reads the stored version itself, so nothing\n * about the design crosses the wire inbound and there is no version of this\n * surface that can publish a design the check has not seen.\n */\nexport function FormDetailCard(props: FormDetailCardProps) {\n const { hostId, formId, basePath, canPublish, hostRoleLoaded } = props\n const firestore = useFirestore()\n const ContactFieldsZone = useConsoleWidgetSlot()\n // Read at render, after the shell has loaded its plugins.\n const hasContactFieldsWidget =\n listConsoleWidgets(FORM_CONTACT_FIELDS_ZONE.id).length > 0\n /**\n * The write a widget in the contact-fields zone asks for. The declaration\n * is one array on the form document, and the document is this plugin's, so\n * the page writes what the widget decided.\n */\n const saveFields = useCallback(\n async (fields: FormFieldDecl[]) => {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n fields,\n updatedAt: Timestamp.now(),\n })\n },\n [firestore, hostId, formId],\n )\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const createHostVersion = useHostVersionApi()\n const promoteForm = useFormPromoteApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { queueLoading } = useLoading()\n\n const { data: form, status } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'forms', formId),\n [firestore, hostId, formId],\n { idField: '$id' },\n )\n // Three states, not two (AGL-706): loading and absent both arrive as\n // `undefined`, and an editable form over a mistyped id reads as data loss.\n const notFound = status !== 'loading' && !form\n\n /**\n * The published design, in BOTH stored forms (AGL-1151).\n *\n * The read above is converter-less, so `nodes` arrives exactly as stored —\n * msgpack for a design promoted since forms were compressed, a plain map\n * for everything older. Decoded once here because two surfaces below need\n * it and both fail silently on the raw value: `checkFormContract` reports\n * bogus violations against a tree it cannot walk, and the AGL-753 empty\n * guard passes on a `Bytes` because `Object.keys` counts the wrapper.\n */\n const formNodes = useMemo(\n () => Aglyn.decodeStoredNodes<Record<string, any>>(form?.nodes),\n [form?.nodes],\n )\n\n // No orderBy: the oldest version docs predate `createdAt`, and Firestore\n // drops documents missing the ordered field.\n const { data: versionDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms', formId, 'versions'),\n limit(100),\n ),\n [firestore, hostId, formId],\n { idField: '$id' },\n )\n const versions = [...(versionDocs ?? [])].sort(\n (a: any, b: any) =>\n (b.createdAt?.seconds ?? 0) - (a.createdAt?.seconds ?? 0),\n )\n const publishedVersionId = form?.versionId as string | undefined\n\n const [name, setName] = useState<string | null>(null)\n const [lead, setLead] = useState<boolean | null>(null)\n const [consentField, setConsentField] = useState<string | null>(null)\n /**\n * The campaigns picked on screen, or null while the stored value stands.\n *\n * Null and `[]` are different: null means \"not touched\", `[]` means \"take\n * this form out of every campaign\". Collapsing them would make clearing the\n * last campaign a no-op, which is the half of set-and-clear that is easy to\n * ship broken.\n */\n const [campaignIds, setCampaignIds] = useState<string[] | null>(null)\n const [opening, setOpening] = useState(false)\n const [promoting, setPromoting] = useState<string | null>(null)\n /**\n * What a REFUSED promotion said, kept on the surface rather than in a toast.\n *\n * A contract violation is a list of things to go and fix in the besigner,\n * and a snackbar is gone by the time the author has read the second one.\n * The besigner's own refusal persists for the same reason.\n */\n const [promoteRefusal, setPromoteRefusal] = useState<{\n message: string\n violations: Aglyn.FormContractViolation[]\n } | null>(null)\n\n /** The fields the PUBLISHED design draws, which is what a submission can carry. */\n const declaredFields: Aglyn.FormFieldDecl[] = useMemo(\n () => (Array.isArray(form?.fields) ? form.fields : []),\n [form],\n )\n\n const effectiveLead = lead ?? form?.routing?.lead === true\n const effectiveConsent = consentField ?? String(form?.consentFieldName ?? '')\n /*\n * Whether lead routing COULD work on this design — the two preconditions\n * `checkFormContract` applies at publish, asked here so the switch says so\n * before it is flipped rather than after (AGL-2612). Consent is read as\n * it is being edited: picking a field below is what satisfies it.\n */\n const hasEmailField = formFieldsCanYieldAnEmail(declaredFields)\n const capturesConsent = formFieldsCaptureConsent(declaredFields, effectiveConsent)\n /*\n * Where this form's people are: the contacts list narrowed to this form,\n * asked of the plugin that keeps people (`null`, and no link, without one). Built from the resolved org slug and\n * subdomain the besigner link already waits on, so the link appears when\n * they land and never points at a half-built address.\n */\n const contactsHref =\n orgSlug && host\n ? pluginRecordFilteredHref('contact', { orgSlug, host }, 'form', formId)\n : null\n /*\n * WHICH CAMPAIGNS THIS FORM IS PART OF.\n *\n * The site's campaigns are read while this page is open because the field\n * is DISPLAYED here as well as edited here: the document stores ids, and a\n * page that drew them until a button was pressed would be showing a\n * merchant raw storage. Bounded at the ceiling the campaigns table itself\n * draws, so it can offer nothing that list does not.\n */\n const siteCampaigns = useHostCampaigns(hostId, { enabled: Boolean(form) })\n const storedCampaigns = useMemo(\n () => Aglyn.readCampaignIds(form as Record<string, unknown>),\n [form],\n )\n const effectiveCampaigns = campaignIds ?? storedCampaigns\n\n /**\n * What the published design would break, judged against the declaration as\n * it is being edited here.\n *\n * Shown here as well as at publish because the two halves are edited in\n * different places: turning lead routing on for a form whose design has no\n * email field is a break the author causes HERE, on a design they are not\n * looking at, and the besigner would not tell them until they next opened\n * it.\n */\n const violations = useMemo(\n () =>\n form\n ? Aglyn.checkFormContract({\n form: {\n routing: { ...(form.routing ?? {}), lead: effectiveLead },\n ...(effectiveConsent\n ? { consentFieldName: effectiveConsent }\n : {}),\n },\n formId,\n nodes: formNodes as never,\n formNodeId: findFormNodeId(formNodes as never),\n })\n : [],\n [form, formId, effectiveLead, effectiveConsent],\n )\n\n const handleSave = useCallback(async () => {\n const dequeue = queueLoading()\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n // A rename carries the keys the Forms list searches by (AGL-3330), or\n // the form would be findable only by the name it used to have.\n ...(name != null\n ? {\n displayName: name.trim(),\n ...Aglyn.formListFields({\n id: formId,\n displayName: name.trim(),\n slug: form?.slug,\n }),\n }\n : {}),\n ...(lead != null ? { routing: { ...(form?.routing ?? {}), lead } } : {}),\n ...(consentField != null\n ? { consentFieldName: consentField.trim() }\n : {}),\n // An empty selection is stored as an empty array rather than removing\n // the field: one shape for \"in no campaign\", which is what keeps this\n // writer and the campaign's own detach agreeing.\n ...(campaignIds != null\n ? {\n [Aglyn.CAMPAIGN_MEMBERSHIP_FIELD]:\n Aglyn.campaignMembershipValue(campaignIds),\n }\n : {}),\n updatedAt: Timestamp.now(),\n })\n setName(null)\n setLead(null)\n setConsentField(null)\n setCampaignIds(null)\n enqueueSnackbar('Form saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n }, [\n firestore,\n hostId,\n formId,\n form,\n name,\n lead,\n consentField,\n campaignIds,\n queueLoading,\n enqueueSnackbar,\n ])\n\n /**\n * Makes one version the version the live sites serve.\n *\n * The route decides, not this handler: it re-reads the stored version, runs\n * `checkFormContract` on the tree it is about to write, and answers 422 with\n * the violations when a publish would stop the submissions arriving. So the\n * only thing here is what to do with each answer — and a refusal is put on\n * the page rather than in a toast, because it is a list of things to fix.\n */\n const handlePromote = useCallback(\n (targetVersionId: string) => async () => {\n if (promoting) return\n setPromoting(targetVersionId)\n setPromoteRefusal(null)\n try {\n const result = await promoteForm({ hostId, formId, versionId: targetVersionId })\n if (!result.ok) {\n const message = result.message ?? 'Publish failed'\n setPromoteRefusal({\n message,\n violations: result.violations ?? [],\n })\n // `persist` because this is a refusal an author has to act on: an\n // auto-dismissed warning is how someone walks away believing the\n // form shipped.\n enqueueSnackbar(message, {\n variant: 'warning',\n allowDuplicate: true,\n persist: true,\n })\n return\n }\n enqueueSnackbar(\n 'Published. The live sites now serve this version, and the form’s ' +\n 'declared fields match it.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Could not publish that version', {\n variant: 'error',\n allowDuplicate: true,\n persist: true,\n })\n } finally {\n setPromoting(null)\n }\n },\n [promoting, promoteForm, hostId, formId, enqueueSnackbar],\n )\n\n /**\n * Opens a version in the besigner, minting the first one when the form has\n * none.\n *\n * Forms adopted from a page carry a declared field list and a `nodes` map\n * seeded from the page they were adopted from — so the first version opens\n * on the form the site was ALREADY submitting rather than a blank canvas.\n */\n const handleOpen = useCallback(\n (targetVersionId?: string) => async () => {\n if (opening) return\n setOpening(true)\n try {\n let versionId = targetVersionId ?? publishedVersionId ?? versions[0]?.$id\n if (!versionId) {\n versionId = Aglyn.createResourceUid()\n // Rides /api/hosts/versions: rules deny the client create, and the\n // route allows a resource's FIRST version on every plan — which\n // this always is, since the form reached here with no `versionId`.\n await createHostVersion({\n hostId,\n kind: 'form',\n parentId: formId,\n id: versionId,\n data: {\n formId,\n hostId,\n displayName: 'Initial version',\n // Emptiness, not just absence: a form that holds no `nodes` and\n // `??` would pass an empty map straight through to a version\n // that opens uneditable (AGL-753). Measured on the DECODED\n // tree — `Object.keys` over a stored `Bytes` is non-empty, so\n // the raw value passes this guard and mints exactly the\n // uneditable version it exists to stop (AGL-1151).\n nodes: Object.keys(formNodes ?? {}).length\n ? formNodes\n : {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: [],\n },\n },\n ...(form?.rootId ? { rootId: form.rootId } : {}),\n },\n })\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n versionId,\n updatedAt: Timestamp.now(),\n })\n }\n // The editor's URL needs the resolved org slug and subdomain, which\n // `useConsoleHostRoute` reads. Until they land the button below is\n // disabled, so this branch cannot be reached without them.\n if (orgSlug && host) {\n router.push(\n buildRoute(Route.FORM_BESIGNER, { orgSlug, host, formId, versionId }),\n )\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Could not open the besigner', {\n variant: 'error',\n allowDuplicate: true,\n })\n setOpening(false)\n }\n },\n [\n opening,\n createHostVersion,\n publishedVersionId,\n versions,\n firestore,\n hostId,\n formId,\n form,\n router,\n orgSlug,\n host,\n enqueueSnackbar,\n ],\n )\n\n const dirty =\n name != null ||\n lead != null ||\n consentField != null ||\n (campaignIds != null &&\n !Aglyn.campaignMembershipUnchanged(storedCampaigns, campaignIds))\n // Named before it is used on every row, so a role denial reads as a reason\n // rather than as a control that does nothing.\n const publishBlock = hostRoleLoaded\n ? 'Your role on this site can edit content but not publish it'\n : 'Checking what your role can do…'\n\n if (notFound) {\n return (\n <CardDisplay\n header=\"Form not found\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'A form is created from the Forms list and keeps the id every ' +\n 'submission is filed under.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2} sx={{ alignItems: 'flex-start' }}>\n <Alert severity=\"info\">\n {`No form on this site has the id ${formId}. It may have been deleted, or the link may be from another site.`}\n </Alert>\n {basePath ? <AppLink href={basePath}>{'Back to forms'}</AppLink> : null}\n </Stack>\n </CardDisplay>\n )\n }\n\n /* The cards, named so the page chrome above them is a plain list of\n what this surface publishes upward. */\n const cards = (\n <GridItems\n spacing={3}\n items={[\n {\n size: { xs: 12, lg: 5 },\n children: (\n <CardDisplay\n header=\"Details\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'The id is what every submission is filed under, so ' +\n 'renaming a form never splits its history.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <TextField\n label=\"Display name\"\n size=\"small\"\n value={name ?? form?.displayName ?? ''}\n onChange={(event) => setName(event.target.value)}\n fullWidth\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`ID ${formId} — every submission is filed under this, so it never changes`}\n </Typography>\n {/*\n A FORM IS PART OF A CAMPAIGN, and may be part of several.\n The same signup form is placed by one push and re-placed by\n the next, so this is a multi-select — a single value would\n make filing it under this quarter's campaign silently\n un-file it from last quarter's.\n\n It is an assignment and nothing else: it does not decide who\n a campaign mails, and it does not credit a submission to\n one. Attribution is recorded from the link a visitor\n followed, on its own evidence, and it goes on saying what it\n says whatever is picked here.\n */}\n <CampaignPicker\n options={siteCampaigns.options}\n value={effectiveCampaigns}\n onChange={setCampaignIds}\n helperText=\"The campaigns this form is part of. It does not change who a campaign mails.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n />\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={!dirty}\n onClick={handleSave}\n >\n {'Save'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n ),\n },\n {\n size: { xs: 12, lg: 7 },\n children: (\n <CardDisplay\n header=\"CRM routing\"\n help={pluginDocsHelp('forms', {\n anchor: '#where-submissions-go',\n excerpt:\n 'Every submission reaches the Inbox and updates the ' +\n 'contact. Lead routing and the consent field decide ' +\n 'what else happens to it.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {/*\n THE PART THAT IS NOT A SWITCH (AGL-2612). Every capture\n lands in Contacts whatever is set below; the switch decides\n only whether a LEAD is filed beside the contact. Said\n first, because the switch on its own read as the only way\n a submission reached the CRM.\n */}\n <Typography variant=\"body2\">\n {'Every submission with an email address updates the person ' +\n 'in Contacts at stage Lead — a new contact when the ' +\n 'address is new, an interaction on their timeline when it ' +\n 'is not. A customer stays a customer.'}\n </Typography>\n <Stack spacing={0.5}>\n <FormControlLabel\n control={\n <Switch\n checked={effectiveLead}\n onChange={(event) => setLead(event.target.checked)}\n />\n }\n label=\"Also create a lead from the address someone gives this form\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {!hasEmailField\n ? 'Needs an email field in the published design — a ' +\n 'submission without an address cannot key a lead, and ' +\n 'publishing with this on would be refused until one exists.'\n : !capturesConsent\n ? 'Needs a marketing consent field — a lead nobody opted ' +\n 'in with is one the team cannot email, and publishing ' +\n 'with this on would be refused until the field below ' +\n 'names one.'\n : 'Files a lead in CRM → Leads for the sales team to work, ' +\n 'carrying the opt-in the consent field records.'}\n </Typography>\n </Stack>\n <TextField\n select\n label=\"Marketing consent field\"\n size=\"small\"\n value={effectiveConsent}\n onChange={(event) => setConsentField(event.target.value)}\n fullWidth\n helperText=\"The one field that IS the opt-in. Submitting a form is never itself consent.\"\n >\n <MenuItem value=\"\">\n {'None — this form collects no opt-in'}\n </MenuItem>\n {declaredFields.map((field) => (\n <MenuItem key={field.fieldName} value={field.fieldName}>\n {field.label\n ? `${field.label} (${field.fieldName})`\n : field.fieldName}\n </MenuItem>\n ))}\n </TextField>\n {violations.length ? (\n <Alert severity=\"warning\">\n <AlertTitle>{'This form would stop collecting'}</AlertTitle>\n <Stack component=\"ul\" sx={{ pl: 2, m: 0 }}>\n {violations.map((violation) => (\n <Typography\n component=\"li\"\n variant=\"body2\"\n key={`${violation.code}:${violation.nodeId ?? violation.fieldName ?? ''}`}\n >\n {violation.message}\n </Typography>\n ))}\n </Stack>\n </Alert>\n ) : null}\n {/*\n The people this form captured, on the list that holds them.\n Withheld until the site's address resolves, for the reason\n the besigner button is.\n */}\n {contactsHref ? (\n <Typography variant=\"body2\">\n <AppLink href={contactsHref}>\n {'See the contacts this form captured in the CRM'}\n </AppLink>\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n ),\n },\n // Where each field saves on the person: another plugin's card, in a\n // zone this page hosts. No item at all where nothing fills it, so the\n // grid keeps no empty cell.\n ...(ContactFieldsZone && hasContactFieldsWidget\n ? [\n {\n size: { xs: 12 },\n children: (\n <ContactFieldsZone\n slot={FORM_CONTACT_FIELDS_ZONE.id}\n hostId={hostId}\n formId={formId}\n fields={declaredFields}\n loading={status === 'loading'}\n saveFields={saveFields}\n />\n ),\n },\n ]\n : []),\n {\n size: { xs: 12, lg: 5 },\n children: (\n <FormMetricsCard\n stats={form?.stats}\n fields={declaredFields}\n leadRouting={effectiveLead}\n loading={status === 'loading'}\n />\n ),\n },\n {\n size: { xs: 12, lg: 7 },\n children: (\n <CardDisplay\n header=\"Versions\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'Publishing a form version updates every page that ' +\n 'places it. Saving does not — a save is not a publish.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {promoteRefusal ? (\n <Alert\n severity=\"warning\"\n onClose={() => setPromoteRefusal(null)}\n >\n <AlertTitle>{promoteRefusal.message}</AlertTitle>\n {promoteRefusal.violations.length ? (\n <Stack component=\"ul\" sx={{ pl: 2, m: 0 }}>\n {promoteRefusal.violations.map((violation) => (\n <Typography\n component=\"li\"\n variant=\"body2\"\n key={`${violation.code}:${violation.nodeId ?? violation.fieldName ?? ''}`}\n >\n {violation.message}\n </Typography>\n ))}\n </Stack>\n ) : null}\n </Alert>\n ) : null}\n {versions.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No versions yet — opening the besigner creates the first one.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Version'}</TableCell>\n <TableCell>{'Created'}</TableCell>\n <TableCell>{'Updated'}</TableCell>\n <TableCell align=\"right\">{'Actions'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {versions.map((version: any) => (\n <TableRow key={version.$id} hover>\n <TableCell>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"body2\">\n {version.displayName ?? version.$id}\n </Typography>\n {version.$id === publishedVersionId ? (\n <Chip\n label=\"Current\"\n color=\"success\"\n size=\"small\"\n />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n {version.createdAt?.toDate?.().toLocaleString() ??\n '--'}\n </TableCell>\n <TableCell>\n {version.updatedAt?.toDate?.().toLocaleString() ??\n '--'}\n </TableCell>\n <TableCell align=\"right\">\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n <Button\n size=\"small\"\n disabled={opening || !orgSlug || !host}\n onClick={handleOpen(version.$id)}\n >\n {'Open'}\n </Button>\n {/*\n The version the sites already serve has nothing\n to promote to, so the control is disabled\n rather than absent — an absent control and an\n inapplicable one look identical, and only one\n of them is honest.\n */}\n <Button\n size=\"small\"\n variant=\"outlined\"\n title={canPublish ? undefined : publishBlock}\n disabled={\n !canPublish ||\n promoting !== null ||\n version.$id === publishedVersionId\n }\n onClick={handlePromote(version.$id)}\n >\n {promoting === version.$id\n ? 'Publishing…'\n : version.$id === publishedVersionId\n ? 'Published'\n : 'Publish'}\n </Button>\n </Stack>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n )}\n </Stack>\n </CardDisplay>\n ),\n },\n {\n /*\n THE MESSAGES THEMSELVES, under the numbers that describe them.\n Full width because it is a table of people and what they wrote,\n and it sits below the metrics because those are read at a glance\n and this is read a row at a time.\n */\n size: { xs: 12 },\n children: <FormSubmissionsCard hostId={hostId} formId={formId} />,\n },\n {\n // Full width: the frame is a document, and a document in a\n // five-column card is a column of one-word lines.\n size: { xs: 12 },\n children: (\n <CardDisplay\n header=\"What a submission will carry\"\n help={pluginDocsHelp('forms', {\n anchor: '#field-types',\n excerpt:\n 'Each field arrives under its own name. Two fields ' +\n 'sharing one name arrive as a single answer.',\n })}\n contentGutterX\n contentGutterY\n >\n <FormDesignPreview\n formId={formId}\n nodes={form?.nodes}\n consentFieldName={\n effectiveConsent ? effectiveConsent : undefined\n }\n loading={status === 'loading'}\n />\n </CardDisplay>\n ),\n },\n ]}\n />\n )\n\n return (\n <>\n {/*\n The page chrome this surface cannot set for itself. The console shell\n owns the layout and builds its heading from the Forms nav item, so\n without these the page about one form is headed `Forms` and its trail\n stops on the list — the same on every row of it.\n */}\n <PageHeaderRecord\n title={form ? form.displayName || formId : undefined}\n />\n {/*\n The besigner is what this surface exists to reach, so it leads — in\n the PAGE header, which is where an action belongs on a surface with\n no section rail beneath it. Withheld when there is no form: it would\n mint a version document under an id that has none (AGL-706).\n */}\n <PageHeaderActions>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={opening || !orgSlug || !host}\n title={\n orgSlug && host ? undefined : 'Resolving this site’s address…'\n }\n onClick={handleOpen()}\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {opening ? 'Opening…' : 'Edit in besigner'}\n </Button>\n </PageHeaderActions>\n {cards}\n </>\n )\n}\nFormDetailCard.displayName = 'FormDetailCard'\n\n/**\n * The `form` node inside a published design.\n *\n * A form document's tree holds exactly one, because the document IS that\n * form — but the tree is a flat map with a synthetic root, so the node has to\n * be found rather than assumed to be the root itself.\n */\nfunction findFormNodeId(\n nodes: Record<string, { componentId?: string }> | undefined,\n): string | undefined {\n return Object.keys(nodes ?? {}).find(\n (id) => nodes?.[id]?.componentId === 'form',\n )\n}\n\nexport default FormDetailCard\n"],"names":["Aglyn","buildRoute","formFieldsCanYieldAnEmail","formFieldsCaptureConsent","PageHeaderActions","PageHeaderRecord","listConsoleWidgets","pluginDocsHelp","Route","useConsoleWidgetSlot","pluginRecordFilteredHref","ICON_VARIANT_BESIGNER","AppLink","CardDisplay","GridItems","MdiIcon","useLoading","ScrollTable","useSnackbar","Timestamp","Alert","AlertTitle","Button","Chip","FormControlLabel","MenuItem","Stack","Switch","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useConsoleHostRoute","useFirestore","useFirestoreCollection","useFirestoreDoc","useHostCampaigns","useHostVersionApi","CampaignPicker","collection","doc","limit","query","updateDoc","useRouter","useCallback","useMemo","useState","FORM_CONTACT_FIELDS_ZONE","FormDesignPreview","FormMetricsCard","FormSubmissionsCard","useFormPromoteApi","FormDetailCard","props","name","form","hostId","formId","basePath","canPublish","hostRoleLoaded","firestore","ContactFieldsZone","hasContactFieldsWidget","id","length","saveFields","fields","updatedAt","now","orgSlug","subdomain","host","createHostVersion","promoteForm","router","enqueueSnackbar","queueLoading","data","status","idField","notFound","formNodes","decodeStoredNodes","nodes","versionDocs","versions","sort","a","b","createdAt","seconds","publishedVersionId","versionId","setName","lead","setLead","consentField","setConsentField","campaignIds","setCampaignIds","opening","setOpening","promoting","setPromoting","promoteRefusal","setPromoteRefusal","declaredFields","Array","isArray","effectiveLead","routing","effectiveConsent","String","consentFieldName","hasEmailField","capturesConsent","contactsHref","siteCampaigns","enabled","Boolean","storedCampaigns","readCampaignIds","effectiveCampaigns","violations","checkFormContract","formNodeId","findFormNodeId","handleSave","dequeue","displayName","trim","formListFields","slug","CAMPAIGN_MEMBERSHIP_FIELD","campaignMembershipValue","variant","persist","error","console","allowDuplicate","handlePromote","targetVersionId","result","ok","message","handleOpen","$id","createResourceUid","kind","parentId","Object","keys","CANVAS_ROOT_ELEMENT_ID","componentId","rootId","push","FORM_BESIGNER","dirty","campaignMembershipUnchanged","publishBlock","header","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","sx","alignItems","severity","href","cards","items","size","xs","lg","children","label","value","onChange","event","target","fullWidth","color","options","helperText","empty","ready","direction","disabled","onClick","control","checked","select","map","field","fieldName","component","pl","m","violation","code","nodeId","slot","loading","stats","leadRouting","onClose","align","version","hover","toDate","toLocaleString","justifyContent","title","undefined","startIcon","path","find"],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
18
|
+
import type { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
|
|
19
|
+
import type { ListQueryDeclaration, ListQueryFilter, ListQueryRequest } from '@aglyn/shared-ui-jsx/const/list-query-plan';
|
|
20
|
+
/** Every filter the forms grid offers, in the `ListFilterField` grammar. */
|
|
21
|
+
export declare const FORM_LIST_FILTER_FIELDS: readonly ListFilterField[];
|
|
22
|
+
/** The forms list's query: its fields, its one order, and its search. */
|
|
23
|
+
export declare const FORM_LIST_QUERY: ListQueryDeclaration;
|
|
24
|
+
/** How each field reads on a chip and as a hidden column's header. */
|
|
25
|
+
export declare const FORM_LIST_FILTER_HEADERS: Readonly<Record<string, string>>;
|
|
26
|
+
/** Status is the stored `retired`, picked by name. */
|
|
27
|
+
export declare const FORM_STATUS_OPTIONS: {
|
|
28
|
+
value: string;
|
|
29
|
+
label: string;
|
|
30
|
+
}[];
|
|
31
|
+
/** Lead routing is the stored switch, picked by name. */
|
|
32
|
+
export declare const FORM_LEAD_ROUTING_OPTIONS: {
|
|
33
|
+
value: string;
|
|
34
|
+
label: string;
|
|
35
|
+
}[];
|
|
36
|
+
/** The fields picked from choices rather than typed. */
|
|
37
|
+
export declare const FORM_LIST_SELECT_FIELDS: readonly ["status", "leadRouting", "campaignIds"];
|
|
38
|
+
/**
|
|
39
|
+
* A retired form is a tombstone the list leaves out unless the reader asks
|
|
40
|
+
* for Status (AGL-2671). On the query, so a page is a page of forms in use
|
|
41
|
+
* rather than one with retired rows dropped from it.
|
|
42
|
+
*/
|
|
43
|
+
export declare const FORM_IN_USE: ListQueryFilter;
|
|
44
|
+
/**
|
|
45
|
+
* What the forms list asks this time: the clauses in force, the search
|
|
46
|
+
* words, and the forms in use unless Status is one of the clauses.
|
|
47
|
+
*/
|
|
48
|
+
export declare function formListRequest(clauses: readonly ListFilterClause[], search: readonly string[]): ListQueryRequest;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
/*==========================================
|
|
18
|
+
* WHAT THE FORMS LIST FILTERS AND SEARCHES BY — ALL OF IT ON THE QUERY
|
|
19
|
+
* (AGL-3330, on the AGL-3321 list query plan).
|
|
20
|
+
*
|
|
21
|
+
* The list pages a site's catalog (`hosts/{hostId}/forms`), so a filter
|
|
22
|
+
* matched over the rows already read would answer "no such form" for one on
|
|
23
|
+
* a later page. Every clause and the search word go on ONE Firestore query
|
|
24
|
+
* (`planListQuery`), paged by that query (`useListQuery`); what one query
|
|
25
|
+
* cannot hold is refused by name, never matched in memory.
|
|
26
|
+
*
|
|
27
|
+
* Each field is one every form stores: the create paths write it
|
|
28
|
+
* (`newFormListFields`), a rename rewrites the search keys
|
|
29
|
+
* (`formListFields`), and `tools/scripts/backfill-form-list-fields.mjs`
|
|
30
|
+
* stamps the forms made before.
|
|
31
|
+
*
|
|
32
|
+
* - Display name: a word of it (`nameTokens`), the shared name-search
|
|
33
|
+
* prefixes every `displayName`-named document carries.
|
|
34
|
+
* - Slug: stored normalized (`normalizeFormSlug`), so it is its own key.
|
|
35
|
+
* - Submissions: the counter `/api/forms/submit` increments on the form, a
|
|
36
|
+
* number or a range. Leads: the same counter's equality — a count, or
|
|
37
|
+
* none. A form that counted nothing holds `null`, never zero, so "is
|
|
38
|
+
* empty" is an equality.
|
|
39
|
+
* - Last submission (epoch ms) and Updated: days, before or after.
|
|
40
|
+
* - Status: `retired`, the queryable mirror of `archivedAt`.
|
|
41
|
+
* - Lead routing: `routing.lead`, a boolean on every form.
|
|
42
|
+
* - Campaign: `campaignIds`, any of the picked campaigns, as stored.
|
|
43
|
+
*
|
|
44
|
+
* ## The index budget, and what it bought
|
|
45
|
+
*
|
|
46
|
+
* One query holds any number of equalities, one array clause and one range,
|
|
47
|
+
* and a range leads the order. Unfiltered, and under equalities alone, the
|
|
48
|
+
* list reads in document order, which Firestore serves by merging its
|
|
49
|
+
* single-field indexes: no composite. A composite is needed only where an
|
|
50
|
+
* equality or array field meets a RANGE, one per (field, range order) pair
|
|
51
|
+
* (`listQueryIndexes`), so the count is (equality fields) × (range orders).
|
|
52
|
+
*
|
|
53
|
+
* Three ranges — Submissions, Last submission, Updated — are the visible
|
|
54
|
+
* columns with no other honest operator. Leads is asked by equality (a
|
|
55
|
+
* count, or none), Display name by a word, Slug exactly, so none of those
|
|
56
|
+
* adds an order; a Leads range would be a fourth order and seven more
|
|
57
|
+
* composites. Eight equality fields (the search tokens, the name tokens,
|
|
58
|
+
* Slug, the two counters' equalities, `retired`, Lead routing, Campaign)
|
|
59
|
+
* times three orders, less the Submissions equality against its own order,
|
|
60
|
+
* is 23. `form-list-query.spec.ts` pins every one against the index file.
|
|
61
|
+
*=========================================*/ /** Every filter the forms grid offers, in the `ListFilterField` grammar. */ export const FORM_LIST_FILTER_FIELDS = [
|
|
62
|
+
{
|
|
63
|
+
column: 'displayName',
|
|
64
|
+
kind: 'text',
|
|
65
|
+
path: 'displayName',
|
|
66
|
+
tokensPath: 'nameTokens',
|
|
67
|
+
operators: [
|
|
68
|
+
'contains'
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
column: 'slug',
|
|
73
|
+
kind: 'text',
|
|
74
|
+
path: 'slug',
|
|
75
|
+
lowerPath: 'slug',
|
|
76
|
+
operators: [
|
|
77
|
+
'equals'
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
column: 'submissions',
|
|
82
|
+
kind: 'number',
|
|
83
|
+
path: 'stats.submissions',
|
|
84
|
+
presence: 'nullable'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
column: 'leads',
|
|
88
|
+
kind: 'number',
|
|
89
|
+
path: 'stats.leads',
|
|
90
|
+
presence: 'nullable',
|
|
91
|
+
operators: [
|
|
92
|
+
'=',
|
|
93
|
+
'isEmpty'
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
column: 'lastSubmission',
|
|
98
|
+
kind: 'date',
|
|
99
|
+
path: 'stats.lastSubmissionAtMs',
|
|
100
|
+
storedAs: 'millis',
|
|
101
|
+
operators: [
|
|
102
|
+
'is',
|
|
103
|
+
'after',
|
|
104
|
+
'onOrAfter',
|
|
105
|
+
'before',
|
|
106
|
+
'onOrBefore',
|
|
107
|
+
'isNotEmpty'
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
column: 'updatedAt',
|
|
112
|
+
kind: 'date',
|
|
113
|
+
path: 'updatedAt',
|
|
114
|
+
presence: 'always'
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
column: 'status',
|
|
118
|
+
kind: 'boolean',
|
|
119
|
+
path: 'retired',
|
|
120
|
+
operators: [
|
|
121
|
+
'equals'
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
column: 'leadRouting',
|
|
126
|
+
kind: 'boolean',
|
|
127
|
+
path: 'routing.lead',
|
|
128
|
+
operators: [
|
|
129
|
+
'equals'
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
column: 'campaignIds',
|
|
134
|
+
kind: 'exact',
|
|
135
|
+
path: 'campaignIds',
|
|
136
|
+
// The array itself: "any of" is one `array-contains-any`.
|
|
137
|
+
tokensPath: 'campaignIds',
|
|
138
|
+
operators: [
|
|
139
|
+
'isAnyOf'
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
];
|
|
143
|
+
/** The forms list's query: its fields, its one order, and its search. */ export const FORM_LIST_QUERY = {
|
|
144
|
+
fields: FORM_LIST_FILTER_FIELDS,
|
|
145
|
+
// Document order, as the list has always read, which needs no composite.
|
|
146
|
+
sorts: [
|
|
147
|
+
{
|
|
148
|
+
path: '__name__',
|
|
149
|
+
direction: 'asc'
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
search: {
|
|
153
|
+
tokensPath: 'searchTokens'
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
/** How each field reads on a chip and as a hidden column's header. */ export const FORM_LIST_FILTER_HEADERS = {
|
|
157
|
+
displayName: 'Display name',
|
|
158
|
+
slug: 'Slug',
|
|
159
|
+
submissions: 'Submissions',
|
|
160
|
+
leads: 'Leads',
|
|
161
|
+
lastSubmission: 'Last submission',
|
|
162
|
+
updatedAt: 'Updated',
|
|
163
|
+
status: 'Status',
|
|
164
|
+
leadRouting: 'Lead routing',
|
|
165
|
+
campaignIds: 'Campaign'
|
|
166
|
+
};
|
|
167
|
+
/** Status is the stored `retired`, picked by name. */ export const FORM_STATUS_OPTIONS = [
|
|
168
|
+
{
|
|
169
|
+
value: 'false',
|
|
170
|
+
label: 'Active'
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
value: 'true',
|
|
174
|
+
label: 'Retired'
|
|
175
|
+
}
|
|
176
|
+
];
|
|
177
|
+
/** Lead routing is the stored switch, picked by name. */ export const FORM_LEAD_ROUTING_OPTIONS = [
|
|
178
|
+
{
|
|
179
|
+
value: 'true',
|
|
180
|
+
label: 'On'
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
value: 'false',
|
|
184
|
+
label: 'Off'
|
|
185
|
+
}
|
|
186
|
+
];
|
|
187
|
+
/** The fields picked from choices rather than typed. */ export const FORM_LIST_SELECT_FIELDS = [
|
|
188
|
+
'status',
|
|
189
|
+
'leadRouting',
|
|
190
|
+
'campaignIds'
|
|
191
|
+
];
|
|
192
|
+
/**
|
|
193
|
+
* A retired form is a tombstone the list leaves out unless the reader asks
|
|
194
|
+
* for Status (AGL-2671). On the query, so a page is a page of forms in use
|
|
195
|
+
* rather than one with retired rows dropped from it.
|
|
196
|
+
*/ export const FORM_IN_USE = {
|
|
197
|
+
path: 'retired',
|
|
198
|
+
op: '==',
|
|
199
|
+
value: false
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* What the forms list asks this time: the clauses in force, the search
|
|
203
|
+
* words, and the forms in use unless Status is one of the clauses.
|
|
204
|
+
*/ export function formListRequest(clauses, search) {
|
|
205
|
+
const askedStatus = clauses.some((clause)=>clause.field === 'status');
|
|
206
|
+
return _extends({
|
|
207
|
+
clauses,
|
|
208
|
+
search
|
|
209
|
+
}, askedStatus ? {} : {
|
|
210
|
+
base: [
|
|
211
|
+
FORM_IN_USE
|
|
212
|
+
]
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//# sourceMappingURL=form-list-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-list-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryRequest,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*==========================================\n * WHAT THE FORMS LIST FILTERS AND SEARCHES BY — ALL OF IT ON THE QUERY\n * (AGL-3330, on the AGL-3321 list query plan).\n *\n * The list pages a site's catalog (`hosts/{hostId}/forms`), so a filter\n * matched over the rows already read would answer \"no such form\" for one on\n * a later page. Every clause and the search word go on ONE Firestore query\n * (`planListQuery`), paged by that query (`useListQuery`); what one query\n * cannot hold is refused by name, never matched in memory.\n *\n * Each field is one every form stores: the create paths write it\n * (`newFormListFields`), a rename rewrites the search keys\n * (`formListFields`), and `tools/scripts/backfill-form-list-fields.mjs`\n * stamps the forms made before.\n *\n * - Display name: a word of it (`nameTokens`), the shared name-search\n * prefixes every `displayName`-named document carries.\n * - Slug: stored normalized (`normalizeFormSlug`), so it is its own key.\n * - Submissions: the counter `/api/forms/submit` increments on the form, a\n * number or a range. Leads: the same counter's equality — a count, or\n * none. A form that counted nothing holds `null`, never zero, so \"is\n * empty\" is an equality.\n * - Last submission (epoch ms) and Updated: days, before or after.\n * - Status: `retired`, the queryable mirror of `archivedAt`.\n * - Lead routing: `routing.lead`, a boolean on every form.\n * - Campaign: `campaignIds`, any of the picked campaigns, as stored.\n *\n * ## The index budget, and what it bought\n *\n * One query holds any number of equalities, one array clause and one range,\n * and a range leads the order. Unfiltered, and under equalities alone, the\n * list reads in document order, which Firestore serves by merging its\n * single-field indexes: no composite. A composite is needed only where an\n * equality or array field meets a RANGE, one per (field, range order) pair\n * (`listQueryIndexes`), so the count is (equality fields) × (range orders).\n *\n * Three ranges — Submissions, Last submission, Updated — are the visible\n * columns with no other honest operator. Leads is asked by equality (a\n * count, or none), Display name by a word, Slug exactly, so none of those\n * adds an order; a Leads range would be a fourth order and seven more\n * composites. Eight equality fields (the search tokens, the name tokens,\n * Slug, the two counters' equalities, `retired`, Lead routing, Campaign)\n * times three orders, less the Submissions equality against its own order,\n * is 23. `form-list-query.spec.ts` pins every one against the index file.\n *=========================================*/\n\n/** Every filter the forms grid offers, in the `ListFilterField` grammar. */\nexport const FORM_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n {\n column: 'displayName',\n kind: 'text',\n path: 'displayName',\n tokensPath: 'nameTokens',\n operators: ['contains'],\n },\n {\n column: 'slug',\n kind: 'text',\n path: 'slug',\n lowerPath: 'slug',\n operators: ['equals'],\n },\n { column: 'submissions', kind: 'number', path: 'stats.submissions', presence: 'nullable' },\n {\n column: 'leads',\n kind: 'number',\n path: 'stats.leads',\n presence: 'nullable',\n operators: ['=', 'isEmpty'],\n },\n {\n column: 'lastSubmission',\n kind: 'date',\n path: 'stats.lastSubmissionAtMs',\n storedAs: 'millis',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore', 'isNotEmpty'],\n },\n { column: 'updatedAt', kind: 'date', path: 'updatedAt', presence: 'always' },\n { column: 'status', kind: 'boolean', path: 'retired', operators: ['equals'] },\n { column: 'leadRouting', kind: 'boolean', path: 'routing.lead', operators: ['equals'] },\n {\n column: 'campaignIds',\n kind: 'exact',\n path: 'campaignIds',\n // The array itself: \"any of\" is one `array-contains-any`.\n tokensPath: 'campaignIds',\n operators: ['isAnyOf'],\n },\n]\n\n/** The forms list's query: its fields, its one order, and its search. */\nexport const FORM_LIST_QUERY: ListQueryDeclaration = {\n fields: FORM_LIST_FILTER_FIELDS,\n // Document order, as the list has always read, which needs no composite.\n sorts: [{ path: '__name__', direction: 'asc' }],\n search: { tokensPath: 'searchTokens' },\n}\n\n/** How each field reads on a chip and as a hidden column's header. */\nexport const FORM_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n slug: 'Slug',\n submissions: 'Submissions',\n leads: 'Leads',\n lastSubmission: 'Last submission',\n updatedAt: 'Updated',\n status: 'Status',\n leadRouting: 'Lead routing',\n campaignIds: 'Campaign',\n}\n\n/** Status is the stored `retired`, picked by name. */\nexport const FORM_STATUS_OPTIONS = [\n { value: 'false', label: 'Active' },\n { value: 'true', label: 'Retired' },\n]\n\n/** Lead routing is the stored switch, picked by name. */\nexport const FORM_LEAD_ROUTING_OPTIONS = [\n { value: 'true', label: 'On' },\n { value: 'false', label: 'Off' },\n]\n\n/** The fields picked from choices rather than typed. */\nexport const FORM_LIST_SELECT_FIELDS = ['status', 'leadRouting', 'campaignIds'] as const\n\n/**\n * A retired form is a tombstone the list leaves out unless the reader asks\n * for Status (AGL-2671). On the query, so a page is a page of forms in use\n * rather than one with retired rows dropped from it.\n */\nexport const FORM_IN_USE: ListQueryFilter = { path: 'retired', op: '==', value: false }\n\n/**\n * What the forms list asks this time: the clauses in force, the search\n * words, and the forms in use unless Status is one of the clauses.\n */\nexport function formListRequest(\n clauses: readonly ListFilterClause[],\n search: readonly string[],\n): ListQueryRequest {\n const askedStatus = clauses.some((clause) => clause.field === 'status')\n return {\n clauses,\n search,\n ...(askedStatus ? {} : { base: [FORM_IN_USE] }),\n }\n}\n"],"names":["FORM_LIST_FILTER_FIELDS","column","kind","path","tokensPath","operators","lowerPath","presence","storedAs","FORM_LIST_QUERY","fields","sorts","direction","search","FORM_LIST_FILTER_HEADERS","displayName","slug","submissions","leads","lastSubmission","updatedAt","status","leadRouting","campaignIds","FORM_STATUS_OPTIONS","value","label","FORM_LEAD_ROUTING_OPTIONS","FORM_LIST_SELECT_FIELDS","FORM_IN_USE","op","formListRequest","clauses","askedStatus","some","clause","field","base"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CA4C2C,GAE3C,0EAA0E,GAC1E,OAAO,MAAMA,0BAAsD;IACjE;QACEC,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNC,YAAY;QACZC,WAAW;YAAC;SAAW;IACzB;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNG,WAAW;QACXD,WAAW;YAAC;SAAS;IACvB;IACA;QAAEJ,QAAQ;QAAeC,MAAM;QAAUC,MAAM;QAAqBI,UAAU;IAAW;IACzF;QACEN,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNI,UAAU;QACVF,WAAW;YAAC;YAAK;SAAU;IAC7B;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNK,UAAU;QACVH,WAAW;YAAC;YAAM;YAAS;YAAa;YAAU;YAAc;SAAa;IAC/E;IACA;QAAEJ,QAAQ;QAAaC,MAAM;QAAQC,MAAM;QAAaI,UAAU;IAAS;IAC3E;QAAEN,QAAQ;QAAUC,MAAM;QAAWC,MAAM;QAAWE,WAAW;YAAC;SAAS;IAAC;IAC5E;QAAEJ,QAAQ;QAAeC,MAAM;QAAWC,MAAM;QAAgBE,WAAW;YAAC;SAAS;IAAC;IACtF;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACN,0DAA0D;QAC1DC,YAAY;QACZC,WAAW;YAAC;SAAU;IACxB;CACD,CAAA;AAED,uEAAuE,GACvE,OAAO,MAAMI,kBAAwC;IACnDC,QAAQV;IACR,yEAAyE;IACzEW,OAAO;QAAC;YAAER,MAAM;YAAYS,WAAW;QAAM;KAAE;IAC/CC,QAAQ;QAAET,YAAY;IAAe;AACvC,EAAC;AAED,oEAAoE,GACpE,OAAO,MAAMU,2BAA6D;IACxEC,aAAa;IACbC,MAAM;IACNC,aAAa;IACbC,OAAO;IACPC,gBAAgB;IAChBC,WAAW;IACXC,QAAQ;IACRC,aAAa;IACbC,aAAa;AACf,EAAC;AAED,oDAAoD,GACpD,OAAO,MAAMC,sBAAsB;IACjC;QAAEC,OAAO;QAASC,OAAO;IAAS;IAClC;QAAED,OAAO;QAAQC,OAAO;IAAU;CACnC,CAAA;AAED,uDAAuD,GACvD,OAAO,MAAMC,4BAA4B;IACvC;QAAEF,OAAO;QAAQC,OAAO;IAAK;IAC7B;QAAED,OAAO;QAASC,OAAO;IAAM;CAChC,CAAA;AAED,sDAAsD,GACtD,OAAO,MAAME,0BAA0B;IAAC;IAAU;IAAe;CAAc,CAAS;AAExF;;;;CAIC,GACD,OAAO,MAAMC,cAA+B;IAAE1B,MAAM;IAAW2B,IAAI;IAAML,OAAO;AAAM,EAAC;AAEvF;;;CAGC,GACD,OAAO,SAASM,gBACdC,OAAoC,EACpCnB,MAAyB;IAEzB,MAAMoB,cAAcD,QAAQE,IAAI,CAAC,CAACC,SAAWA,OAAOC,KAAK,KAAK;IAC9D,OAAO;QACLJ;QACAnB;OACIoB,cAAc,CAAC,IAAI;QAAEI,MAAM;YAACR;SAAY;IAAC;AAEjD"}
|
|
@@ -24,56 +24,35 @@ import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiContentCopy, m
|
|
|
24
24
|
import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
25
25
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
26
26
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
27
|
+
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
27
28
|
import ListTable, { ListRowActions, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
28
|
-
import {
|
|
29
|
+
import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
30
|
+
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
29
31
|
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
30
|
-
import {
|
|
32
|
+
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
31
33
|
import QuotaReadoutComponent from "@aglyn/shared-ui-jsx/components/quota-readout.component";
|
|
32
34
|
import { CreateArtifactDrawer } from "@aglyn/shared-ui-jsx-forms";
|
|
33
35
|
import { Alert, Button, Stack, Typography } from "@mui/material";
|
|
34
|
-
import { collection, doc, updateDoc } from "firebase/firestore";
|
|
35
|
-
import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useHostResourceApi, useLiveArtifactCount
|
|
36
|
-
import {
|
|
36
|
+
import { collection, doc, getCountFromServer, query, updateDoc, where } from "firebase/firestore";
|
|
37
|
+
import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useHostCampaigns, useHostResourceApi, useLiveArtifactCount } from "@aglyn/tenant-feature-instance";
|
|
38
|
+
import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
37
39
|
import { useRouter } from "next/navigation";
|
|
38
|
-
import { useCallback, useMemo, useState } from "react";
|
|
40
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
39
41
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*/ const
|
|
46
|
-
inMemoryListField('displayName', 'text'),
|
|
47
|
-
inMemoryListField('slug', 'text'),
|
|
48
|
-
inMemoryListField('status', 'select', 'statusKey')
|
|
49
|
-
];
|
|
50
|
-
const FORM_FILTER_HEADERS = {
|
|
51
|
-
displayName: 'Display name',
|
|
52
|
-
slug: 'Slug',
|
|
53
|
-
status: 'Status'
|
|
54
|
-
};
|
|
55
|
-
const FORM_FILTER_OPTIONS = {
|
|
56
|
-
status: [
|
|
57
|
-
{
|
|
58
|
-
value: 'active',
|
|
59
|
-
label: 'Active'
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
value: 'retired',
|
|
63
|
-
label: 'Retired'
|
|
64
|
-
}
|
|
65
|
-
]
|
|
66
|
-
};
|
|
67
|
-
const FORM_SEARCH_FIELDS = [
|
|
42
|
+
import { FORM_LEAD_ROUTING_OPTIONS, FORM_LIST_FILTER_FIELDS, FORM_LIST_FILTER_HEADERS, FORM_LIST_QUERY, FORM_LIST_SELECT_FIELDS, FORM_STATUS_OPTIONS, formListRequest } from "./form-list-query.js";
|
|
43
|
+
/**
|
|
44
|
+
* The filterable columns the table shows. The rest of the declaration —
|
|
45
|
+
* Status, Lead routing, Campaign — reaches the Filters panel as hidden
|
|
46
|
+
* columns (`hiddenFilterColumns`).
|
|
47
|
+
*/ const FORM_VISIBLE_FILTER_COLUMNS = [
|
|
68
48
|
'displayName',
|
|
69
|
-
'slug'
|
|
49
|
+
'slug',
|
|
50
|
+
'submissions',
|
|
51
|
+
'leads',
|
|
52
|
+
'lastSubmission',
|
|
53
|
+
'updatedAt'
|
|
70
54
|
];
|
|
71
|
-
const FORM_HIDDEN_COLUMNS =
|
|
72
|
-
status: false
|
|
73
|
-
};
|
|
74
|
-
/** A form row with the status its filter matches on. */ const withStatusKey = (form)=>_extends({}, form, {
|
|
75
|
-
statusKey: isFormArchived(form) ? 'retired' : 'active'
|
|
76
|
-
});
|
|
55
|
+
const FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(FORM_LIST_FILTER_FIELDS, FORM_VISIBLE_FILTER_COLUMNS);
|
|
77
56
|
/**
|
|
78
57
|
* THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.
|
|
79
58
|
*
|
|
@@ -136,6 +115,7 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
136
115
|
const [createError, setCreateError] = useState(null);
|
|
137
116
|
const [creating, setCreating] = useState(false);
|
|
138
117
|
const [retireError, setRetireError] = useState(null);
|
|
118
|
+
/** Bumped by each retire or restore, so the retired count is asked again. */ const [retiredEpoch, setRetiredEpoch] = useState(0);
|
|
139
119
|
/**
|
|
140
120
|
* Retire a form, or put it back (AGL-2671).
|
|
141
121
|
*
|
|
@@ -156,8 +136,11 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
156
136
|
setRetireError(null);
|
|
157
137
|
try {
|
|
158
138
|
await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {
|
|
159
|
-
archivedAt: retired ? Date.now() : null
|
|
139
|
+
archivedAt: retired ? Date.now() : null,
|
|
140
|
+
// The mirror the list's query asks (AGL-3330).
|
|
141
|
+
retired
|
|
160
142
|
});
|
|
143
|
+
setRetiredEpoch((epoch)=>epoch + 1);
|
|
161
144
|
} catch (unused) {
|
|
162
145
|
setRetireError(retired ? 'Could not retire that form. Try again shortly.' : 'Could not restore that form. Try again shortly.');
|
|
163
146
|
}
|
|
@@ -165,76 +148,109 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
165
148
|
firestore,
|
|
166
149
|
hostId
|
|
167
150
|
]);
|
|
168
|
-
|
|
169
|
-
*
|
|
151
|
+
/*
|
|
152
|
+
* THE LIST IS ITS QUERY (AGL-3330, on the AGL-3321 list query plan).
|
|
153
|
+
*
|
|
154
|
+
* The grid's Filters panel and search box edit the clauses and the words;
|
|
155
|
+
* `useListQuery` puts every one of them on ONE Firestore query and pages
|
|
156
|
+
* that query, so a page is a page of matches and a form whose only match
|
|
157
|
+
* is past the first page is still found. What one query cannot hold — a
|
|
158
|
+
* second range, a second array clause — is refused by name above the list
|
|
159
|
+
* (`ListQueryNotices`) rather than matched over the rows read. See
|
|
160
|
+
* `form-list-query.ts` for every field and the indexes they cost.
|
|
170
161
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* documents that HAVE the field, and the resources route stores an
|
|
174
|
-
* allow-list it never checks for presence, so ordering on a name would hide
|
|
175
|
-
* every form created without one rather than mis-sorting the list.
|
|
162
|
+
* Unfiltered it reads in document order, as it always has: `orderBy` on a
|
|
163
|
+
* name would drop every form stored without one rather than mis-sort it.
|
|
176
164
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
|
|
165
|
+
* A retired form is a TOMBSTONE, not a row, unless the reader asks for
|
|
166
|
+
* Status (AGL-2671). The query leaves them out (`retired == false`), so a
|
|
167
|
+
* page is a page of forms in use; a Status clause replaces that with the
|
|
168
|
+
* reader's own choice. Retiring a form removes it from the list, and
|
|
169
|
+
* without the Status filter that would be a one-way door: the row is the
|
|
170
|
+
* only route back to Restore.
|
|
171
|
+
*/ const [formClauses, setFormClauses] = useState([]);
|
|
172
|
+
const gridFilter = useListGridFilter({
|
|
173
|
+
clauses: formClauses,
|
|
174
|
+
onChange: setFormClauses,
|
|
175
|
+
selectFields: FORM_LIST_SELECT_FIELDS
|
|
176
|
+
});
|
|
177
|
+
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0;
|
|
178
|
+
const formsCollection = useMemo(()=>collection(firestore, 'hosts', hostId, 'forms'), [
|
|
180
179
|
firestore,
|
|
181
180
|
hostId
|
|
182
|
-
]
|
|
181
|
+
]);
|
|
182
|
+
const { status, rows: forms, hasMore, page, setPage, pageSize, setPageSize, plan } = useListQuery({
|
|
183
|
+
collection: formsCollection,
|
|
184
|
+
declaration: FORM_LIST_QUERY,
|
|
185
|
+
request: formListRequest(gridFilter.clauses, gridFilter.searchWords),
|
|
186
|
+
deps: [
|
|
187
|
+
firestore,
|
|
188
|
+
hostId
|
|
189
|
+
],
|
|
183
190
|
idField: '$id'
|
|
184
191
|
});
|
|
185
192
|
/*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* route (which carries no `archivedAt` at all) and one archived later are
|
|
190
|
-
* not one value to filter on. The cost is that a tombstone spends a slot in
|
|
191
|
-
* whichever page it falls in.
|
|
193
|
+
* The Campaign filter's choices: the campaigns placed on this site, by
|
|
194
|
+
* name, and any campaign a clause names that the list does not, by its id,
|
|
195
|
+
* so the chip can still be read and cleared.
|
|
192
196
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
* the only route back to Restore.
|
|
202
|
-
*/ const [formClauses, setFormClauses] = useState([]);
|
|
203
|
-
const askedStatus = formClauses.some((clause)=>clause.field === 'status');
|
|
204
|
-
const formRows = useMemo(()=>formWindow.filter((form)=>askedStatus || !isFormArchived(form)).map(withStatusKey), [
|
|
205
|
-
formWindow,
|
|
206
|
-
askedStatus
|
|
207
|
-
]);
|
|
208
|
-
const formWindowRows = useMemo(()=>formData == null ? void 0 : formData.filter((form)=>askedStatus || !isFormArchived(form)).map(withStatusKey), [
|
|
209
|
-
formData,
|
|
210
|
-
askedStatus
|
|
211
|
-
]);
|
|
212
|
-
const formFilter = usePagedRowsFilter({
|
|
213
|
-
data: formWindowRows,
|
|
214
|
-
rows: formRows,
|
|
215
|
-
hasMore,
|
|
216
|
-
page,
|
|
217
|
-
setPage,
|
|
218
|
-
pageSize,
|
|
219
|
-
setPageSize
|
|
220
|
-
}, {
|
|
221
|
-
fields: FORM_FILTER_FIELDS,
|
|
222
|
-
options: FORM_FILTER_OPTIONS,
|
|
223
|
-
headers: FORM_FILTER_HEADERS,
|
|
224
|
-
search: FORM_SEARCH_FIELDS,
|
|
225
|
-
clauses: formClauses,
|
|
226
|
-
onChange: setFormClauses
|
|
197
|
+
* Read once the reader opens the Filters panel, not on every visit: the
|
|
198
|
+
* names cost the org lookup and up to fifty campaign documents, which a
|
|
199
|
+
* reader who came to open one form would pay for a filter they never
|
|
200
|
+
* touched. Until they land the column has no choices, and the panel lists
|
|
201
|
+
* it a moment later.
|
|
202
|
+
*/ const [wantsCampaigns, setWantsCampaigns] = useState(false);
|
|
203
|
+
const siteCampaigns = useHostCampaigns(hostId, {
|
|
204
|
+
enabled: wantsCampaigns || formClauses.some((clause)=>clause.field === 'campaignIds')
|
|
227
205
|
});
|
|
228
|
-
|
|
229
|
-
|
|
206
|
+
const formFilterOptions = useMemo(()=>{
|
|
207
|
+
const named = siteCampaigns.options.map((option)=>({
|
|
208
|
+
value: option.value,
|
|
209
|
+
label: option.label
|
|
210
|
+
}));
|
|
211
|
+
const unnamed = formClauses.filter((clause)=>clause.field === 'campaignIds').flatMap((clause)=>clause.value.split(',')).map((value)=>value.trim()).filter((value)=>value && !named.some((option)=>option.value === value)).map((value)=>({
|
|
212
|
+
value,
|
|
213
|
+
label: value
|
|
214
|
+
}));
|
|
215
|
+
return {
|
|
216
|
+
status: FORM_STATUS_OPTIONS,
|
|
217
|
+
leadRouting: FORM_LEAD_ROUTING_OPTIONS,
|
|
218
|
+
campaignIds: [
|
|
219
|
+
...named,
|
|
220
|
+
...unnamed
|
|
221
|
+
]
|
|
222
|
+
};
|
|
223
|
+
}, [
|
|
224
|
+
siteCampaigns.options,
|
|
225
|
+
formClauses
|
|
226
|
+
]);
|
|
230
227
|
/*
|
|
231
228
|
* The COUNT is a server aggregate, not the length of a page. `forms` is one
|
|
232
229
|
* page, and publishing that as the site's form count would read as room to
|
|
233
230
|
* spare on a site that is already at the ceiling.
|
|
234
231
|
*/ const liveFormCount = useLiveArtifactCount(hostId, 'forms');
|
|
235
|
-
// Pending or refused, the page
|
|
236
|
-
//
|
|
237
|
-
const formsUsed = liveFormCount != null ? liveFormCount :
|
|
232
|
+
// Pending or refused, the page stands in: a LOWER bound, never a confident
|
|
233
|
+
// zero.
|
|
234
|
+
const formsUsed = liveFormCount != null ? liveFormCount : forms.length;
|
|
235
|
+
/*
|
|
236
|
+
* How many forms are retired, for the "keep their slot" line (AGL-2674).
|
|
237
|
+
* An aggregate, because the list no longer reads a retired row unless it is
|
|
238
|
+
* asked for one; asked again after this card retires or restores a form.
|
|
239
|
+
*/ const [retiredCount, setRetiredCount] = useState(null);
|
|
240
|
+
useEffect(()=>{
|
|
241
|
+
let active = true;
|
|
242
|
+
getCountFromServer(query(formsCollection, where('retired', '==', true))).then((snapshot)=>{
|
|
243
|
+
if (active) setRetiredCount(snapshot.data().count);
|
|
244
|
+
}).catch(()=>{
|
|
245
|
+
// The line is a courtesy; without the count it is simply not shown.
|
|
246
|
+
});
|
|
247
|
+
return ()=>{
|
|
248
|
+
active = false;
|
|
249
|
+
};
|
|
250
|
+
}, [
|
|
251
|
+
formsCollection,
|
|
252
|
+
retiredEpoch
|
|
253
|
+
]);
|
|
238
254
|
/**
|
|
239
255
|
* Name first, then create (AGL-700).
|
|
240
256
|
*
|
|
@@ -385,6 +401,27 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
385
401
|
return (value == null ? void 0 : (_value_toLocaleString = value.toLocaleString) == null ? void 0 : _value_toLocaleString.call(value)) || '--';
|
|
386
402
|
}
|
|
387
403
|
},
|
|
404
|
+
/*
|
|
405
|
+
* Status and Lead routing, hidden until Columns shows them (AGL-3330).
|
|
406
|
+
* Each is a select the Filters panel offers either way, and draws the
|
|
407
|
+
* choice its value names. Status reads the FACT, `archivedAt`, rather
|
|
408
|
+
* than the `retired` mirror the query asks, so a form the backfill has
|
|
409
|
+
* not reached yet still reads true.
|
|
410
|
+
*/ {
|
|
411
|
+
field: 'status',
|
|
412
|
+
headerName: 'Status',
|
|
413
|
+
minWidth: 110,
|
|
414
|
+
valueGetter: (_value, row)=>String(isFormArchived(row))
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
field: 'leadRouting',
|
|
418
|
+
headerName: 'Lead routing',
|
|
419
|
+
minWidth: 120,
|
|
420
|
+
valueGetter: (_value, row)=>{
|
|
421
|
+
var _row_routing;
|
|
422
|
+
return String((row == null ? void 0 : (_row_routing = row.routing) == null ? void 0 : _row_routing.lead) === true);
|
|
423
|
+
}
|
|
424
|
+
},
|
|
388
425
|
listActionsColumn((row)=>{
|
|
389
426
|
var _form_displayName;
|
|
390
427
|
const form = _extends({}, row, {
|
|
@@ -499,7 +536,7 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
499
536
|
limit: Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit,
|
|
500
537
|
noun: "form"
|
|
501
538
|
}),
|
|
502
|
-
|
|
539
|
+
retiredCount ? /*#__PURE__*/ _jsx(Typography, {
|
|
503
540
|
variant: "caption",
|
|
504
541
|
color: "text.secondary",
|
|
505
542
|
children: 'Retired forms keep their slot'
|
|
@@ -542,21 +579,43 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
542
579
|
},
|
|
543
580
|
children: retireError
|
|
544
581
|
}) : null,
|
|
545
|
-
/*#__PURE__*/ _jsx(ListFilterChips,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
582
|
+
/*#__PURE__*/ _jsx(ListFilterChips, {
|
|
583
|
+
fields: FORM_LIST_FILTER_FIELDS,
|
|
584
|
+
headers: FORM_LIST_FILTER_HEADERS,
|
|
585
|
+
clauses: gridFilter.clauses,
|
|
586
|
+
onChange: gridFilter.setClauses,
|
|
587
|
+
options: formFilterOptions
|
|
588
|
+
}),
|
|
589
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
590
|
+
refused: listQueryRefusals(plan.refused, {
|
|
591
|
+
fields: FORM_LIST_FILTER_FIELDS,
|
|
592
|
+
headers: FORM_LIST_FILTER_HEADERS,
|
|
593
|
+
options: formFilterOptions
|
|
594
|
+
}),
|
|
595
|
+
notices: plan.notices
|
|
596
|
+
}),
|
|
551
597
|
/*#__PURE__*/ _jsx(ListTable, _extends({
|
|
552
598
|
rowHeight: TABLE_ROW_HEIGHT,
|
|
553
|
-
columns:
|
|
599
|
+
columns: listFilterGridColumns(columns, FORM_LIST_FILTER_FIELDS, formFilterOptions, FORM_LIST_FILTER_HEADERS),
|
|
554
600
|
initialState: {
|
|
555
601
|
columns: {
|
|
556
602
|
columnVisibilityModel: FORM_HIDDEN_COLUMNS
|
|
557
603
|
}
|
|
604
|
+
},
|
|
605
|
+
/*
|
|
606
|
+
* The grid must NOT also filter. The query answers the panel and
|
|
607
|
+
* the search, so a second pass in the browser could only drop rows
|
|
608
|
+
* that already matched — it compares what a column DRAWS, and the
|
|
609
|
+
* query compares what the document stores.
|
|
610
|
+
*/ filterMode: "server",
|
|
611
|
+
filterModel: gridFilter.filterModel,
|
|
612
|
+
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
613
|
+
quickFilter: true,
|
|
614
|
+
// The Campaign filter's names are read once the panel is opened.
|
|
615
|
+
onPreferencePanelOpen: (params)=>{
|
|
616
|
+
if (params.openedPanelValue === 'filters') setWantsCampaigns(true);
|
|
558
617
|
}
|
|
559
|
-
},
|
|
618
|
+
}, filtering ? {
|
|
560
619
|
noRowsLabel: 'No forms match these filters'
|
|
561
620
|
} : {
|
|
562
621
|
noRowsLabel: 'No forms yet',
|
|
@@ -577,7 +636,14 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
577
636
|
// Paged by the footer below, so the grid must not also slice.
|
|
578
637
|
hideFooter: true
|
|
579
638
|
})),
|
|
580
|
-
/*#__PURE__*/ _jsx(ListPagination,
|
|
639
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
640
|
+
page: page,
|
|
641
|
+
pageSize: pageSize,
|
|
642
|
+
rowCount: forms.length,
|
|
643
|
+
hasMore: hasMore,
|
|
644
|
+
onPageChange: setPage,
|
|
645
|
+
onPageSizeChange: setPageSize
|
|
646
|
+
}),
|
|
581
647
|
/*#__PURE__*/ _jsx(CreateArtifactDrawer, {
|
|
582
648
|
open: createOpen,
|
|
583
649
|
onClose: ()=>setCreateOpen(false),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/host-forms-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 * as Aglyn from '@aglyn/aglyn'\nimport type { AglynOrgBilling } from '@aglyn/aglyn'\nimport {\n buildRoute,\n isFormArchived,\n PageHeaderActions,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { ICON_VARIANT_SHOW_DETAIL } from '@aglyn/shared-data-enums'\nimport {\n mdiArchiveArrowDownOutline,\n mdiArchiveArrowUpOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiVectorSquare,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } 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 ListTable, {\n ListRowActions,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n inMemoryListField,\n type ListFilterClause,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { usePagedRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-paged-rows-filter'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, updateDoc } from 'firebase/firestore'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostResourceApi,\n useLiveArtifactCount,\n usePagedCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { collectionPage } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/*\n * What the forms grid's Filters panel offers (AGL-3317). The list is a paged\n * listener, so a filter matches over what its window has read, which the\n * first filter widens (`usePagedRowsFilter`). Status is a hidden column\n * reading `statusKey`, whether the form is retired.\n */\nconst FORM_FILTER_FIELDS = [\n inMemoryListField('displayName', 'text'),\n inMemoryListField('slug', 'text'),\n inMemoryListField('status', 'select', 'statusKey'),\n]\nconst FORM_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n slug: 'Slug',\n status: 'Status',\n}\nconst FORM_FILTER_OPTIONS = {\n status: [\n { value: 'active', label: 'Active' },\n { value: 'retired', label: 'Retired' },\n ],\n}\nconst FORM_SEARCH_FIELDS = ['displayName', 'slug'] as const\nconst FORM_HIDDEN_COLUMNS = { status: false }\n\n/** A form row with the status its filter matches on. */\nconst withStatusKey = (form: any) => ({\n ...form,\n statusKey: isFormArchived(form) ? 'retired' : 'active',\n})\n\nexport interface HostFormsCardProps {\n hostId: string\n /**\n * The Forms surface's own absolute console path, from the shell.\n *\n * A row's link is this plus the form's id, resolved synchronously. The\n * alternative is `useConsoleHostRoute`, which answers `null` for a paint\n * while it reads two documents — and a table whose every row links to\n * `/null/...` on first render is worse than one that pays nothing.\n */\n basePath?: string\n /**\n * The org billing document, from the shell.\n *\n * The quota readout's denominator resolves from it. `undefined` means the\n * plan is not known rather than that it is Free — the shell holds this\n * surface behind a spinner until the org read settles, so it reads that way\n * only when the read failed — and the readout says so instead of naming a\n * cap it cannot support.\n */\n org?: Partial<AglynOrgBilling>\n}\n\n/**\n * THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.\n *\n * The list is the components list's table, deliberately and not\n * approximately: `ListTable` gives it the row grammar every artifact list has\n * — the row opens the detail page, rows are not selectable, one quick action\n * then the overflow — and `ListPagination` gives it the console's one footer.\n * A form is an artifact like a component, so a reader who has used one list\n * has used this one.\n *\n * ## Why the empty state is a table and not a sentence\n *\n * A paragraph and a button reads as a smaller feature than this is: a form is\n * a thing with a slug, a submission count and a version history, and none of\n * that would be visible until the reader had already committed to making one.\n * Rendering the columns with the empty overlay inside them teaches the shape\n * of the artifact BEFORE the first one exists, which is what the components\n * list has always done.\n *\n * ## The two numeric columns, and when one is honestly blank\n *\n * `stats.submissions` and `stats.leads` are both incremented by\n * `/api/forms/submit` on a write that was happening anyway, so both are real\n * numbers. `leads` is counted only for a form whose `routing.lead` is set, so\n * a form that does not route to leads carries no figure at all and renders as\n * a dash rather than as `0`. A zero would say this form has produced no leads,\n * which for an unrouted form is a measurement nobody took.\n */\nexport function HostFormsCard(props: HostFormsCardProps) {\n const { hostId, basePath, org } = props\n const router = useRouter()\n // Duplicate (AGL-2936): the copy is a form of its own — its submissions\n // start empty — and opens on its detail page once made.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) => router.push(formHref(copy.id)),\n })\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n /**\n * The shell's zone renderer (AGL-3043), for other ways to start a form; `null`\n * outside the console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n\n /**\n * One form's page, beneath this surface's own path.\n *\n * `basePath` is the shell's answer and needs no read; the route table is the\n * fallback for a caller that has none, and it is the one that can be `null`\n * for a paint.\n */\n const formHref = useCallback(\n (formId: string) =>\n basePath\n ? `${basePath}/${formId}`\n : buildRoute(Route.FORM_DETAILS, {\n orgSlug: orgSlug ?? '',\n host: host ?? '',\n formId,\n }),\n [basePath, orgSlug, host],\n )\n\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const [creating, setCreating] = useState(false)\n const [retireError, setRetireError] = useState<string | null>(null)\n\n /**\n * Retire a form, or put it back (AGL-2671).\n *\n * A write of one field, client-side like every other edit on this document:\n * the rules already admit it for a host writer, fencing off only `stats`.\n *\n * ⛔ NOT a delete, and the difference is the whole feature. The submissions,\n * the leads and the contact timeline they built are filed under this form,\n * so the document is what makes that history readable; deleting it to stop\n * collection would throw away the reason the merchant kept the form.\n *\n * `Date.now()` is the browser's clock rather than `serverTimestamp()`\n * because {@link isFormArchived} asks for a number greater than zero, and a\n * server sentinel reads as `null` on the local echo — the row would stay\n * looking live until the write round-tripped. The value is a marker, never\n * an audit fact: nothing computes a duration from it.\n */\n const setRetired = useCallback(\n async (formId: string, retired: boolean) => {\n setRetireError(null)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n archivedAt: retired ? Date.now() : null,\n })\n } catch {\n setRetireError(\n retired\n ? 'Could not retire that form. Try again shortly.'\n : 'Could not restore that form. Try again shortly.',\n )\n }\n },\n [firestore, hostId],\n )\n\n /**\n * The list PAGES, over an ordered walk.\n *\n * `collectionPage` holds the ordering decision and the reason it is the\n * document id rather than `displayName` — briefly, `orderBy` matches only\n * documents that HAVE the field, and the resources route stores an\n * allow-list it never checks for presence, so ordering on a name would hide\n * every form created without one rather than mis-sorting the list.\n *\n * The page is NOT re-sorted: sorting a server-ordered window in the browser\n * is what makes a pseudo-random sample look like the first page.\n */\n const {\n status,\n data: formData,\n rows: formWindow,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n collectionPage(\n collection(firestore, 'hosts', hostId, 'forms'),\n pageLimit,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n /*\n * A retired form is a TOMBSTONE, not a row — unless the reader asks for the\n * tombstones (AGL-2671). Client-side because it has to be: Firestore cannot\n * ask for the ABSENCE of a field, so a form created through the resources\n * route (which carries no `archivedAt` at all) and one archived later are\n * not one value to filter on. The cost is that a tombstone spends a slot in\n * whichever page it falls in.\n *\n * The Filters panel's Status is how the reader asks (AGL-3317). With no\n * Status clause the list leaves the tombstones out, as it always has, and\n * reads one page; a Status clause hands the panel every row the window\n * read, retired included, and the clause itself picks which to show. That\n * default is the absence of a clause rather than a clause the list starts\n * with, because a starting clause would widen the window on every visit to\n * answer a question nobody asked. Retiring a form removes it from the list,\n * and without the Status filter that would be a one-way door: the row is\n * the only route back to Restore.\n */\n const [formClauses, setFormClauses] = useState<ListFilterClause[]>([])\n const askedStatus = formClauses.some((clause) => clause.field === 'status')\n const formRows = useMemo(\n () =>\n formWindow\n .filter((form: any) => askedStatus || !isFormArchived(form))\n .map(withStatusKey),\n [formWindow, askedStatus],\n )\n const formWindowRows = useMemo(\n () =>\n formData\n ?.filter((form: any) => askedStatus || !isFormArchived(form))\n .map(withStatusKey),\n [formData, askedStatus],\n )\n const formFilter = usePagedRowsFilter<any>(\n {\n data: formWindowRows,\n rows: formRows,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n },\n {\n fields: FORM_FILTER_FIELDS,\n options: FORM_FILTER_OPTIONS,\n headers: FORM_FILTER_HEADERS,\n search: FORM_SEARCH_FIELDS,\n clauses: formClauses,\n onChange: setFormClauses,\n },\n )\n // The rows on screen: the page, or the page of the matches.\n const forms = formFilter.rows\n\n /*\n * The COUNT is a server aggregate, not the length of a page. `forms` is one\n * page, and publishing that as the site's form count would read as room to\n * spare on a site that is already at the ceiling.\n */\n const liveFormCount = useLiveArtifactCount(hostId, 'forms')\n // Pending or refused, the page window stands in: a LOWER bound, never a\n // confident zero.\n const formsUsed = liveFormCount ?? formWindow.length\n\n /**\n * Name first, then create (AGL-700).\n *\n * A form is created with BOTH halves seeded. `fields` is the declaration the\n * submission path reads and starts empty; the canvas is the design, seeded\n * with a root and a form node already bound to this id — so the besigner\n * opens on something that satisfies `checkFormContract` rather than on a\n * blank page whose first publish is a list of violations.\n */\n const handleCreate = useCallback(\n async (values: Record<string, any>) => {\n if (creating) return\n setCreating(true)\n setCreateError(null)\n try {\n const formId = Aglyn.createResourceUid()\n await createHostResource({\n hostId,\n resource: 'form',\n id: formId,\n data: {\n displayName: values['displayName'],\n slug: Aglyn.normalizeFormSlug(values['displayName']) || formId,\n fields: [],\n rootId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n nodes: {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: ['formRoot'],\n },\n formRoot: {\n $id: 'formRoot',\n componentId: 'form',\n pluginId: BUNDLE_ID,\n parentId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n props: { formId, formName: values['displayName'] },\n nodes: [],\n },\n },\n },\n })\n setCreateOpen(false)\n router.push(formHref(formId))\n } catch (error) {\n console.error(error)\n setCreateError('Could not create that form')\n } finally {\n setCreating(false)\n }\n },\n [creating, createHostResource, hostId, router, formHref],\n )\n\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Display name',\n minWidth: 220,\n type: 'string',\n renderCell: ({ id, value }: any) => (\n <AppLink href={formHref(id as string)}>\n {value || (id as string)}\n </AppLink>\n ),\n },\n {\n field: 'slug',\n headerName: 'Slug',\n minWidth: 160,\n flex: 1,\n type: 'string',\n // Blank reads as a rendering gap; '--' reads as \"nothing here\".\n valueFormatter: (value: any) => value || '--',\n },\n {\n field: 'submissions',\n headerName: 'Submissions',\n minWidth: 130,\n type: 'number',\n // Head AND body. `type: 'number'` right-aligns both, and the explicit\n // pair says so at the call site rather than relying on a grid default\n // that a later `renderCell` would silently override.\n align: 'right',\n headerAlign: 'right',\n valueGetter: (_value: any, row: any) => row?.stats?.submissions ?? null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'leads',\n headerName: 'Leads',\n minWidth: 100,\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n /*\n * A dash rather than a `0` whenever the field is absent.\n *\n * `stats.leads` is incremented only for a form whose `routing.lead` is\n * set, so a form that does not route to leads has no recorded figure.\n * Rendering `0` there would read as \"this form has produced no leads\",\n * which is a claim about a measurement nobody took.\n */\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.leads === 'number' ? row.stats.leads : null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'lastSubmission',\n headerName: 'Last submission',\n minWidth: 170,\n flex: 1,\n type: 'date',\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.lastSubmissionAtMs === 'number'\n ? new Date(row.stats.lastSubmissionAtMs)\n : null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n minWidth: 170,\n flex: 1,\n type: 'date',\n // MUI X v9 passes the value positionally. The v6 object form silently\n // destructures undefined off a Date and every row renders '--'.\n valueGetter: (value: any) => value?.toDate?.() ?? null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n listActionsColumn((row: any) => {\n const form = { ...row, $id: row.$id as string }\n const versionId = form.versionId as string | undefined\n return (\n <ListRowActions\n label={form.displayName ?? form.$id}\n quick={{\n icon: mdiEyeOutline.path,\n label: 'Preview',\n // A form with no version has never been opened in the besigner, so\n // there is no snapshot to render. Disabled and saying so, rather\n // than a link to an empty preview.\n ...(versionId && orgSlug && host\n ? {\n to: buildRoute(Route.FORM_PREVIEW, {\n orgSlug,\n host,\n formId: form.$id,\n versionId,\n }),\n }\n : {\n unavailableReason: versionId\n ? 'Resolving this site’s address…'\n : 'Nothing to preview yet — open it in the besigner once.',\n }),\n }}\n items={[\n {\n key: 'details',\n label: 'View details',\n icon: <MdiIcon path={ICON_VARIANT_SHOW_DETAIL.path} size={0.8} />,\n href: formHref(form.$id),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiVectorSquare.path} size={0.8} />,\n /*\n * A LINK only once the form has a version. A form that has never\n * been opened has none, and opening it MINTS the first one —\n * that is a write, and the detail page is the one place that\n * decides what an initial version looks like. Sending the reader\n * there rather than minting a second way is what keeps the two\n * from drifting.\n */\n href:\n versionId && orgSlug && host\n ? buildRoute(Route.FORM_BESIGNER, {\n orgSlug,\n host,\n formId: form.$id,\n versionId,\n })\n : formHref(form.$id),\n },\n /*\n * Retire / Restore (AGL-2671). Last in the menu, and the only item\n * here that writes.\n *\n * The label says what it DOES to the form rather than what it does\n * to this list — \"Archive\" reads like a filing action, and the\n * consequence a merchant needs to weigh is that the form stops\n * collecting. `/api/forms/submit` refuses a retired form, so one\n * still placed on a published page will turn visitors away.\n */\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('form', {\n id: form.$id,\n name: form.displayName ?? '',\n }),\n },\n isFormArchived(form)\n ? {\n key: 'restore',\n label: 'Restore — start collecting again',\n icon: <MdiIcon path={mdiArchiveArrowUpOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, false),\n }\n : {\n key: 'retire',\n label: 'Retire — stop collecting, keep the history',\n icon: <MdiIcon path={mdiArchiveArrowDownOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, true),\n },\n ]}\n />\n )\n }),\n ]\n\n return (\n <>\n {duplicate.dialog}\n {/*\n The readout leads the create button, in the PAGE header — where Sites,\n screens, layouts, components and templates put theirs. Forms declares\n no sections, so it has no vertical rail for a card-header cluster to\n belong beside, and the controls are about the whole surface rather\n than about anything the card is showing.\n Published from the card because the card owns what they say: the count\n comes from the listener already open here, and a page that counted for\n itself would be a second source for one fact and a second read for one\n collection. It publishes from the LIST, so a form's own detail route —\n a different component — leaves the header with nothing to create into.\n */}\n <PageHeaderActions>\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'center' }}>\n {/*\n The denominator is `formsPerHost` — the allowance\n `/api/hosts/resources` refuses the create at, resolved through the\n same `checkQuota` the route calls, so the number offered and the\n number enforced cannot drift apart. `FORMS_MAX_PER_HOST` bounds a\n READ of the catalog and sits above every plan's allowance, so\n quoting it advertises room the server will not honor.\n */}\n <QuotaReadoutComponent\n ready={org != null}\n used={formsUsed}\n limit={Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit}\n noun=\"form\"\n />\n {/*\n A retired form keeps its slot, and the readout says so rather than\n leaving a merchant to work out why a catalog of three is using five\n (AGL-2674). The count comes from the same aggregate\n `/api/hosts/resources` enforces on, which subtracts deleted forms\n and nothing else — so retirement is deliberately not a way to buy\n room back, and the number here is the number the server will\n refuse a create at.\n\n Shown only when a retirement is actually in the loaded page: the\n line answers a question nobody is asking on a site that has never\n retired anything. It makes no numeric claim, because one page is a\n lower bound on how many there are.\n */}\n {formWindow.some((form: any) => isFormArchived(form)) ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Retired forms keep their slot'}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n {/*\n Other ways to start a form, from plugins (AGL-3043): the\n `hostForms` zone, drawn through the shell's own gated slot. A\n plugin page cannot mount that slot itself, so the shell hands it\n down, and a widget here passes the gates a console page's would.\n */}\n {CreateZone ? (\n <CreateZone slot=\"hostForms\" hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={creating}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {creating ? 'Creating…' : 'Create Form'}\n </Button>\n </Stack>\n </Stack>\n </PageHeaderActions>\n <CardDisplay\n header=\"Forms\"\n help={pluginDocsHelp('forms', {\n anchor: '#find-a-form',\n excerpt:\n 'A form collects submissions into the Inbox, and its design is ' +\n 'drawn in the besigner like any other artifact.',\n })}\n >\n {retireError ? (\n <Alert severity=\"error\" sx={{ mb: 2 }}>\n {retireError}\n </Alert>\n ) : null}\n <ListFilterChips {...formFilter.chipsProps} />\n {formFilter.filtering && hasMore ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Filtering the ${formFilter.read} forms read so far — the next page reads more.`}\n </Typography>\n ) : null}\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={formFilter.filterColumns(columns)}\n initialState={{ columns: { columnVisibilityModel: FORM_HIDDEN_COLUMNS } }}\n // The panel and the search are the grid's; the card answers them\n // over what its window read.\n {...formFilter.gridProps}\n {...(formFilter.filtering\n ? { noRowsLabel: 'No forms match these filters' }\n : {\n noRowsLabel: 'No forms yet',\n noRowsDescription:\n 'A form collects submissions, dedupes the people who send them, and can route them to a lead. Its design is drawn in the besigner and published like any other artifact.',\n noRowsAction: (\n <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create your first form'}\n </Button>\n ),\n })}\n rows={forms}\n // The whole row opens the detail page; the action cluster stops\n // propagation so a menu click never navigates underneath it.\n onOpen={(id) => router.push(formHref(String(id)))}\n // An empty table while the read is in flight reads as \"you have none\"\n // rather than \"these are on their way\".\n loading={status === 'loading'}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n <ListPagination {...formFilter.pagination} />\n {/*\n The console's own create drawer, from the shared library rather than a\n second one that looks like it. The empty state and the header open the\n SAME one, so the state that says \"creating…\" is one state.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new form\"\n // A form document stores no description: `/api/hosts/resources` filters\n // `data` through a per-kind allow-list, so one typed here is dropped\n // without a word.\n includeDescription={false}\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {createError}\n </Alert>\n ) : null\n }\n />\n </CardDisplay>\n </>\n )\n}\nHostFormsCard.displayName = 'HostFormsCard'\n\nexport default HostFormsCard\n"],"names":["Aglyn","buildRoute","isFormArchived","PageHeaderActions","pluginDocsHelp","Route","useConsoleWidgetSlot","ICON_VARIANT_SHOW_DETAIL","mdiArchiveArrowDownOutline","mdiArchiveArrowUpOutline","mdiContentCopy","mdiEyeOutline","mdiVectorSquare","AppLink","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListTable","ListRowActions","listActionsColumn","inMemoryListField","TABLE_ROW_HEIGHT","usePagedRowsFilter","QuotaReadoutComponent","CreateArtifactDrawer","Alert","Button","Stack","Typography","collection","doc","updateDoc","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostResourceApi","useLiveArtifactCount","usePagedCollection","collectionPage","useRouter","useCallback","useMemo","useState","BUNDLE_ID","FORM_FILTER_FIELDS","FORM_FILTER_HEADERS","displayName","slug","status","FORM_FILTER_OPTIONS","value","label","FORM_SEARCH_FIELDS","FORM_HIDDEN_COLUMNS","withStatusKey","form","statusKey","HostFormsCard","props","hostId","basePath","org","router","duplicate","onDuplicated","_kind","copy","push","formHref","id","orgSlug","subdomain","host","firestore","createHostResource","CreateZone","formId","FORM_DETAILS","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","retireError","setRetireError","setRetired","retired","archivedAt","Date","now","data","formData","rows","formWindow","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","formClauses","setFormClauses","askedStatus","some","clause","field","formRows","filter","map","formWindowRows","formFilter","fields","options","headers","search","clauses","onChange","forms","liveFormCount","formsUsed","length","handleCreate","values","createResourceUid","resource","normalizeFormSlug","rootId","CANVAS_ROOT_ELEMENT_ID","nodes","$id","componentId","formRoot","pluginId","parentId","formName","error","console","columns","headerName","minWidth","type","renderCell","href","flex","valueFormatter","align","headerAlign","valueGetter","_value","row","stats","submissions","toLocaleString","leads","lastSubmissionAtMs","toDate","versionId","quick","icon","path","to","FORM_PREVIEW","unavailableReason","items","key","size","FORM_BESIGNER","onClick","request","name","dialog","direction","spacing","sx","alignItems","ready","used","limit","checkQuota","noun","variant","color","slot","orgId","disabled","header","help","anchor","excerpt","severity","mb","chipsProps","filtering","read","rowHeight","filterColumns","initialState","columnVisibilityModel","gridProps","noRowsLabel","noRowsDescription","noRowsAction","onOpen","String","loading","hideFooter","pagination","open","onClose","title","includeDescription","onSubmit","errorSlot","mt"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/host-forms-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 * as Aglyn from '@aglyn/aglyn'\nimport type { AglynOrgBilling } from '@aglyn/aglyn'\nimport {\n buildRoute,\n isFormArchived,\n PageHeaderActions,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { ICON_VARIANT_SHOW_DETAIL } from '@aglyn/shared-data-enums'\nimport {\n mdiArchiveArrowDownOutline,\n mdiArchiveArrowUpOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiVectorSquare,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } 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 ListTable, {\n ListRowActions,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n type ListFilterClause,\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 QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n doc,\n getCountFromServer,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostCampaigns,\n useHostResourceApi,\n useLiveArtifactCount,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n FORM_LEAD_ROUTING_OPTIONS,\n FORM_LIST_FILTER_FIELDS,\n FORM_LIST_FILTER_HEADERS,\n FORM_LIST_QUERY,\n FORM_LIST_SELECT_FIELDS,\n FORM_STATUS_OPTIONS,\n formListRequest,\n} from './form-list-query'\n\n/**\n * The filterable columns the table shows. The rest of the declaration —\n * Status, Lead routing, Campaign — reaches the Filters panel as hidden\n * columns (`hiddenFilterColumns`).\n */\nconst FORM_VISIBLE_FILTER_COLUMNS = [\n 'displayName',\n 'slug',\n 'submissions',\n 'leads',\n 'lastSubmission',\n 'updatedAt',\n]\nconst FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(\n FORM_LIST_FILTER_FIELDS,\n FORM_VISIBLE_FILTER_COLUMNS,\n)\n\nexport interface HostFormsCardProps {\n hostId: string\n /**\n * The Forms surface's own absolute console path, from the shell.\n *\n * A row's link is this plus the form's id, resolved synchronously. The\n * alternative is `useConsoleHostRoute`, which answers `null` for a paint\n * while it reads two documents — and a table whose every row links to\n * `/null/...` on first render is worse than one that pays nothing.\n */\n basePath?: string\n /**\n * The org billing document, from the shell.\n *\n * The quota readout's denominator resolves from it. `undefined` means the\n * plan is not known rather than that it is Free — the shell holds this\n * surface behind a spinner until the org read settles, so it reads that way\n * only when the read failed — and the readout says so instead of naming a\n * cap it cannot support.\n */\n org?: Partial<AglynOrgBilling>\n}\n\n/**\n * THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.\n *\n * The list is the components list's table, deliberately and not\n * approximately: `ListTable` gives it the row grammar every artifact list has\n * — the row opens the detail page, rows are not selectable, one quick action\n * then the overflow — and `ListPagination` gives it the console's one footer.\n * A form is an artifact like a component, so a reader who has used one list\n * has used this one.\n *\n * ## Why the empty state is a table and not a sentence\n *\n * A paragraph and a button reads as a smaller feature than this is: a form is\n * a thing with a slug, a submission count and a version history, and none of\n * that would be visible until the reader had already committed to making one.\n * Rendering the columns with the empty overlay inside them teaches the shape\n * of the artifact BEFORE the first one exists, which is what the components\n * list has always done.\n *\n * ## The two numeric columns, and when one is honestly blank\n *\n * `stats.submissions` and `stats.leads` are both incremented by\n * `/api/forms/submit` on a write that was happening anyway, so both are real\n * numbers. `leads` is counted only for a form whose `routing.lead` is set, so\n * a form that does not route to leads carries no figure at all and renders as\n * a dash rather than as `0`. A zero would say this form has produced no leads,\n * which for an unrouted form is a measurement nobody took.\n */\nexport function HostFormsCard(props: HostFormsCardProps) {\n const { hostId, basePath, org } = props\n const router = useRouter()\n // Duplicate (AGL-2936): the copy is a form of its own — its submissions\n // start empty — and opens on its detail page once made.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) => router.push(formHref(copy.id)),\n })\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n /**\n * The shell's zone renderer (AGL-3043), for other ways to start a form; `null`\n * outside the console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n\n /**\n * One form's page, beneath this surface's own path.\n *\n * `basePath` is the shell's answer and needs no read; the route table is the\n * fallback for a caller that has none, and it is the one that can be `null`\n * for a paint.\n */\n const formHref = useCallback(\n (formId: string) =>\n basePath\n ? `${basePath}/${formId}`\n : buildRoute(Route.FORM_DETAILS, {\n orgSlug: orgSlug ?? '',\n host: host ?? '',\n formId,\n }),\n [basePath, orgSlug, host],\n )\n\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const [creating, setCreating] = useState(false)\n const [retireError, setRetireError] = useState<string | null>(null)\n /** Bumped by each retire or restore, so the retired count is asked again. */\n const [retiredEpoch, setRetiredEpoch] = useState(0)\n\n /**\n * Retire a form, or put it back (AGL-2671).\n *\n * A write of one field, client-side like every other edit on this document:\n * the rules already admit it for a host writer, fencing off only `stats`.\n *\n * ⛔ NOT a delete, and the difference is the whole feature. The submissions,\n * the leads and the contact timeline they built are filed under this form,\n * so the document is what makes that history readable; deleting it to stop\n * collection would throw away the reason the merchant kept the form.\n *\n * `Date.now()` is the browser's clock rather than `serverTimestamp()`\n * because {@link isFormArchived} asks for a number greater than zero, and a\n * server sentinel reads as `null` on the local echo — the row would stay\n * looking live until the write round-tripped. The value is a marker, never\n * an audit fact: nothing computes a duration from it.\n */\n const setRetired = useCallback(\n async (formId: string, retired: boolean) => {\n setRetireError(null)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n archivedAt: retired ? Date.now() : null,\n // The mirror the list's query asks (AGL-3330).\n retired,\n })\n setRetiredEpoch((epoch) => epoch + 1)\n } catch {\n setRetireError(\n retired\n ? 'Could not retire that form. Try again shortly.'\n : 'Could not restore that form. Try again shortly.',\n )\n }\n },\n [firestore, hostId],\n )\n\n /*\n * THE LIST IS ITS QUERY (AGL-3330, on the AGL-3321 list query plan).\n *\n * The grid's Filters panel and search box edit the clauses and the words;\n * `useListQuery` puts every one of them on ONE Firestore query and pages\n * that query, so a page is a page of matches and a form whose only match\n * is past the first page is still found. What one query cannot hold — a\n * second range, a second array clause — is refused by name above the list\n * (`ListQueryNotices`) rather than matched over the rows read. See\n * `form-list-query.ts` for every field and the indexes they cost.\n *\n * Unfiltered it reads in document order, as it always has: `orderBy` on a\n * name would drop every form stored without one rather than mis-sort it.\n *\n * A retired form is a TOMBSTONE, not a row, unless the reader asks for\n * Status (AGL-2671). The query leaves them out (`retired == false`), so a\n * page is a page of forms in use; a Status clause replaces that with the\n * reader's own choice. Retiring a form removes it from the list, and\n * without the Status filter that would be a one-way door: the row is the\n * only route back to Restore.\n */\n const [formClauses, setFormClauses] = useState<ListFilterClause[]>([])\n const gridFilter = useListGridFilter({\n clauses: formClauses,\n onChange: setFormClauses,\n selectFields: FORM_LIST_SELECT_FIELDS,\n })\n const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0\n const formsCollection = useMemo(\n () => collection(firestore, 'hosts', hostId, 'forms'),\n [firestore, hostId],\n )\n const {\n status,\n rows: forms,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: formsCollection,\n declaration: FORM_LIST_QUERY,\n request: formListRequest(gridFilter.clauses, gridFilter.searchWords),\n deps: [firestore, hostId],\n idField: '$id',\n })\n\n /*\n * The Campaign filter's choices: the campaigns placed on this site, by\n * name, and any campaign a clause names that the list does not, by its id,\n * so the chip can still be read and cleared.\n *\n * Read once the reader opens the Filters panel, not on every visit: the\n * names cost the org lookup and up to fifty campaign documents, which a\n * reader who came to open one form would pay for a filter they never\n * touched. Until they land the column has no choices, and the panel lists\n * it a moment later.\n */\n const [wantsCampaigns, setWantsCampaigns] = useState(false)\n const siteCampaigns = useHostCampaigns(hostId, {\n enabled: wantsCampaigns || formClauses.some((clause) => clause.field === 'campaignIds'),\n })\n const formFilterOptions = useMemo<Record<string, ListFilterOption[]>>(() => {\n const named = siteCampaigns.options.map((option) => ({\n value: option.value,\n label: option.label,\n }))\n const unnamed = formClauses\n .filter((clause) => clause.field === 'campaignIds')\n .flatMap((clause) => clause.value.split(','))\n .map((value) => value.trim())\n .filter((value) => value && !named.some((option) => option.value === value))\n .map((value) => ({ value, label: value }))\n return {\n status: FORM_STATUS_OPTIONS,\n leadRouting: FORM_LEAD_ROUTING_OPTIONS,\n campaignIds: [...named, ...unnamed],\n }\n }, [siteCampaigns.options, formClauses])\n\n /*\n * The COUNT is a server aggregate, not the length of a page. `forms` is one\n * page, and publishing that as the site's form count would read as room to\n * spare on a site that is already at the ceiling.\n */\n const liveFormCount = useLiveArtifactCount(hostId, 'forms')\n // Pending or refused, the page stands in: a LOWER bound, never a confident\n // zero.\n const formsUsed = liveFormCount ?? forms.length\n /*\n * How many forms are retired, for the \"keep their slot\" line (AGL-2674).\n * An aggregate, because the list no longer reads a retired row unless it is\n * asked for one; asked again after this card retires or restores a form.\n */\n const [retiredCount, setRetiredCount] = useState<number | null>(null)\n useEffect(() => {\n let active = true\n getCountFromServer(query(formsCollection, where('retired', '==', true)))\n .then((snapshot) => {\n if (active) setRetiredCount(snapshot.data().count)\n })\n .catch(() => {\n // The line is a courtesy; without the count it is simply not shown.\n })\n return () => {\n active = false\n }\n }, [formsCollection, retiredEpoch])\n\n /**\n * Name first, then create (AGL-700).\n *\n * A form is created with BOTH halves seeded. `fields` is the declaration the\n * submission path reads and starts empty; the canvas is the design, seeded\n * with a root and a form node already bound to this id — so the besigner\n * opens on something that satisfies `checkFormContract` rather than on a\n * blank page whose first publish is a list of violations.\n */\n const handleCreate = useCallback(\n async (values: Record<string, any>) => {\n if (creating) return\n setCreating(true)\n setCreateError(null)\n try {\n const formId = Aglyn.createResourceUid()\n await createHostResource({\n hostId,\n resource: 'form',\n id: formId,\n data: {\n displayName: values['displayName'],\n slug: Aglyn.normalizeFormSlug(values['displayName']) || formId,\n fields: [],\n rootId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n nodes: {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: ['formRoot'],\n },\n formRoot: {\n $id: 'formRoot',\n componentId: 'form',\n pluginId: BUNDLE_ID,\n parentId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n props: { formId, formName: values['displayName'] },\n nodes: [],\n },\n },\n },\n })\n setCreateOpen(false)\n router.push(formHref(formId))\n } catch (error) {\n console.error(error)\n setCreateError('Could not create that form')\n } finally {\n setCreating(false)\n }\n },\n [creating, createHostResource, hostId, router, formHref],\n )\n\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Display name',\n minWidth: 220,\n type: 'string',\n renderCell: ({ id, value }: any) => (\n <AppLink href={formHref(id as string)}>\n {value || (id as string)}\n </AppLink>\n ),\n },\n {\n field: 'slug',\n headerName: 'Slug',\n minWidth: 160,\n flex: 1,\n type: 'string',\n // Blank reads as a rendering gap; '--' reads as \"nothing here\".\n valueFormatter: (value: any) => value || '--',\n },\n {\n field: 'submissions',\n headerName: 'Submissions',\n minWidth: 130,\n type: 'number',\n // Head AND body. `type: 'number'` right-aligns both, and the explicit\n // pair says so at the call site rather than relying on a grid default\n // that a later `renderCell` would silently override.\n align: 'right',\n headerAlign: 'right',\n valueGetter: (_value: any, row: any) => row?.stats?.submissions ?? null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'leads',\n headerName: 'Leads',\n minWidth: 100,\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n /*\n * A dash rather than a `0` whenever the field is absent.\n *\n * `stats.leads` is incremented only for a form whose `routing.lead` is\n * set, so a form that does not route to leads has no recorded figure.\n * Rendering `0` there would read as \"this form has produced no leads\",\n * which is a claim about a measurement nobody took.\n */\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.leads === 'number' ? row.stats.leads : null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'lastSubmission',\n headerName: 'Last submission',\n minWidth: 170,\n flex: 1,\n type: 'date',\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.lastSubmissionAtMs === 'number'\n ? new Date(row.stats.lastSubmissionAtMs)\n : null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n minWidth: 170,\n flex: 1,\n type: 'date',\n // MUI X v9 passes the value positionally. The v6 object form silently\n // destructures undefined off a Date and every row renders '--'.\n valueGetter: (value: any) => value?.toDate?.() ?? null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n /*\n * Status and Lead routing, hidden until Columns shows them (AGL-3330).\n * Each is a select the Filters panel offers either way, and draws the\n * choice its value names. Status reads the FACT, `archivedAt`, rather\n * than the `retired` mirror the query asks, so a form the backfill has\n * not reached yet still reads true.\n */\n {\n field: 'status',\n headerName: 'Status',\n minWidth: 110,\n valueGetter: (_value: any, row: any) => String(isFormArchived(row)),\n },\n {\n field: 'leadRouting',\n headerName: 'Lead routing',\n minWidth: 120,\n valueGetter: (_value: any, row: any) => String(row?.routing?.lead === true),\n },\n listActionsColumn((row: any) => {\n const form = { ...row, $id: row.$id as string }\n const versionId = form.versionId as string | undefined\n return (\n <ListRowActions\n label={form.displayName ?? form.$id}\n quick={{\n icon: mdiEyeOutline.path,\n label: 'Preview',\n // A form with no version has never been opened in the besigner, so\n // there is no snapshot to render. Disabled and saying so, rather\n // than a link to an empty preview.\n ...(versionId && orgSlug && host\n ? {\n to: buildRoute(Route.FORM_PREVIEW, {\n orgSlug,\n host,\n formId: form.$id,\n versionId,\n }),\n }\n : {\n unavailableReason: versionId\n ? 'Resolving this site’s address…'\n : 'Nothing to preview yet — open it in the besigner once.',\n }),\n }}\n items={[\n {\n key: 'details',\n label: 'View details',\n icon: <MdiIcon path={ICON_VARIANT_SHOW_DETAIL.path} size={0.8} />,\n href: formHref(form.$id),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiVectorSquare.path} size={0.8} />,\n /*\n * A LINK only once the form has a version. A form that has never\n * been opened has none, and opening it MINTS the first one —\n * that is a write, and the detail page is the one place that\n * decides what an initial version looks like. Sending the reader\n * there rather than minting a second way is what keeps the two\n * from drifting.\n */\n href:\n versionId && orgSlug && host\n ? buildRoute(Route.FORM_BESIGNER, {\n orgSlug,\n host,\n formId: form.$id,\n versionId,\n })\n : formHref(form.$id),\n },\n /*\n * Retire / Restore (AGL-2671). Last in the menu, and the only item\n * here that writes.\n *\n * The label says what it DOES to the form rather than what it does\n * to this list — \"Archive\" reads like a filing action, and the\n * consequence a merchant needs to weigh is that the form stops\n * collecting. `/api/forms/submit` refuses a retired form, so one\n * still placed on a published page will turn visitors away.\n */\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('form', {\n id: form.$id,\n name: form.displayName ?? '',\n }),\n },\n isFormArchived(form)\n ? {\n key: 'restore',\n label: 'Restore — start collecting again',\n icon: <MdiIcon path={mdiArchiveArrowUpOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, false),\n }\n : {\n key: 'retire',\n label: 'Retire — stop collecting, keep the history',\n icon: <MdiIcon path={mdiArchiveArrowDownOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, true),\n },\n ]}\n />\n )\n }),\n ]\n\n return (\n <>\n {duplicate.dialog}\n {/*\n The readout leads the create button, in the PAGE header — where Sites,\n screens, layouts, components and templates put theirs. Forms declares\n no sections, so it has no vertical rail for a card-header cluster to\n belong beside, and the controls are about the whole surface rather\n than about anything the card is showing.\n Published from the card because the card owns what they say: the count\n comes from the listener already open here, and a page that counted for\n itself would be a second source for one fact and a second read for one\n collection. It publishes from the LIST, so a form's own detail route —\n a different component — leaves the header with nothing to create into.\n */}\n <PageHeaderActions>\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'center' }}>\n {/*\n The denominator is `formsPerHost` — the allowance\n `/api/hosts/resources` refuses the create at, resolved through the\n same `checkQuota` the route calls, so the number offered and the\n number enforced cannot drift apart. `FORMS_MAX_PER_HOST` bounds a\n READ of the catalog and sits above every plan's allowance, so\n quoting it advertises room the server will not honor.\n */}\n <QuotaReadoutComponent\n ready={org != null}\n used={formsUsed}\n limit={Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit}\n noun=\"form\"\n />\n {/*\n A retired form keeps its slot, and the readout says so rather than\n leaving a merchant to work out why a catalog of three is using five\n (AGL-2674). The count comes from the same aggregate\n `/api/hosts/resources` enforces on, which subtracts deleted forms\n and nothing else — so retirement is deliberately not a way to buy\n room back, and the number here is the number the server will\n refuse a create at.\n\n Shown only when the site has retired a form (an aggregate — the\n list reads no retired row unless asked): the line answers a\n question nobody is asking on a site that has never retired\n anything.\n */}\n {retiredCount ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Retired forms keep their slot'}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n {/*\n Other ways to start a form, from plugins (AGL-3043): the\n `hostForms` zone, drawn through the shell's own gated slot. A\n plugin page cannot mount that slot itself, so the shell hands it\n down, and a widget here passes the gates a console page's would.\n */}\n {CreateZone ? (\n <CreateZone slot=\"hostForms\" hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={creating}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {creating ? 'Creating…' : 'Create Form'}\n </Button>\n </Stack>\n </Stack>\n </PageHeaderActions>\n <CardDisplay\n header=\"Forms\"\n help={pluginDocsHelp('forms', {\n anchor: '#find-a-form',\n excerpt:\n 'A form collects submissions into the Inbox, and its design is ' +\n 'drawn in the besigner like any other artifact.',\n })}\n >\n {retireError ? (\n <Alert severity=\"error\" sx={{ mb: 2 }}>\n {retireError}\n </Alert>\n ) : null}\n <ListFilterChips\n fields={FORM_LIST_FILTER_FIELDS}\n headers={FORM_LIST_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={formFilterOptions}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, {\n fields: FORM_LIST_FILTER_FIELDS,\n headers: FORM_LIST_FILTER_HEADERS,\n options: formFilterOptions,\n })}\n notices={plan.notices}\n />\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={listFilterGridColumns(\n columns,\n FORM_LIST_FILTER_FIELDS,\n formFilterOptions,\n FORM_LIST_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: FORM_HIDDEN_COLUMNS } }}\n /*\n * The grid must NOT also filter. The query answers the panel and\n * the search, so a second pass in the browser could only drop rows\n * that already matched — it compares what a column DRAWS, and the\n * query compares what the document stores.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The Campaign filter's names are read once the panel is opened.\n onPreferencePanelOpen={(params) => {\n if (params.openedPanelValue === 'filters') setWantsCampaigns(true)\n }}\n {...(filtering\n ? { noRowsLabel: 'No forms match these filters' }\n : {\n noRowsLabel: 'No forms yet',\n noRowsDescription:\n 'A form collects submissions, dedupes the people who send them, and can route them to a lead. Its design is drawn in the besigner and published like any other artifact.',\n noRowsAction: (\n <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create your first form'}\n </Button>\n ),\n })}\n rows={forms}\n // The whole row opens the detail page; the action cluster stops\n // propagation so a menu click never navigates underneath it.\n onOpen={(id) => router.push(formHref(String(id)))}\n // An empty table while the read is in flight reads as \"you have none\"\n // rather than \"these are on their way\".\n loading={status === 'loading'}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={forms.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n {/*\n The console's own create drawer, from the shared library rather than a\n second one that looks like it. The empty state and the header open the\n SAME one, so the state that says \"creating…\" is one state.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new form\"\n // A form document stores no description: `/api/hosts/resources` filters\n // `data` through a per-kind allow-list, so one typed here is dropped\n // without a word.\n includeDescription={false}\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {createError}\n </Alert>\n ) : null\n }\n />\n </CardDisplay>\n </>\n )\n}\nHostFormsCard.displayName = 'HostFormsCard'\n\nexport default HostFormsCard\n"],"names":["Aglyn","buildRoute","isFormArchived","PageHeaderActions","pluginDocsHelp","Route","useConsoleWidgetSlot","ICON_VARIANT_SHOW_DETAIL","mdiArchiveArrowDownOutline","mdiArchiveArrowUpOutline","mdiContentCopy","mdiEyeOutline","mdiVectorSquare","AppLink","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","ListRowActions","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","QuotaReadoutComponent","CreateArtifactDrawer","Alert","Button","Stack","Typography","collection","doc","getCountFromServer","query","updateDoc","where","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostCampaigns","useHostResourceApi","useLiveArtifactCount","useListQuery","useRouter","useCallback","useEffect","useMemo","useState","BUNDLE_ID","FORM_LEAD_ROUTING_OPTIONS","FORM_LIST_FILTER_FIELDS","FORM_LIST_FILTER_HEADERS","FORM_LIST_QUERY","FORM_LIST_SELECT_FIELDS","FORM_STATUS_OPTIONS","formListRequest","FORM_VISIBLE_FILTER_COLUMNS","FORM_HIDDEN_COLUMNS","HostFormsCard","props","hostId","basePath","org","router","duplicate","onDuplicated","_kind","copy","push","formHref","id","orgSlug","subdomain","host","firestore","createHostResource","CreateZone","formId","FORM_DETAILS","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","retireError","setRetireError","retiredEpoch","setRetiredEpoch","setRetired","retired","archivedAt","Date","now","epoch","formClauses","setFormClauses","gridFilter","clauses","onChange","selectFields","filtering","length","searchWords","formsCollection","status","rows","forms","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","deps","idField","wantsCampaigns","setWantsCampaigns","siteCampaigns","enabled","some","clause","field","formFilterOptions","named","options","map","option","value","label","unnamed","filter","flatMap","split","trim","leadRouting","campaignIds","liveFormCount","formsUsed","retiredCount","setRetiredCount","active","then","snapshot","data","count","catch","handleCreate","values","createResourceUid","resource","displayName","slug","normalizeFormSlug","fields","rootId","CANVAS_ROOT_ELEMENT_ID","nodes","$id","componentId","formRoot","pluginId","parentId","formName","error","console","columns","headerName","minWidth","type","renderCell","href","flex","valueFormatter","align","headerAlign","valueGetter","_value","row","stats","submissions","toLocaleString","leads","lastSubmissionAtMs","toDate","String","routing","lead","form","versionId","quick","icon","path","to","FORM_PREVIEW","unavailableReason","items","key","size","FORM_BESIGNER","onClick","name","dialog","direction","spacing","sx","alignItems","ready","used","limit","checkQuota","noun","variant","color","slot","orgId","disabled","header","help","anchor","excerpt","severity","mb","headers","setClauses","refused","notices","rowHeight","initialState","columnVisibilityModel","filterMode","filterModel","onFilterModelChange","quickFilter","onPreferencePanelOpen","params","openedPanelValue","noRowsLabel","noRowsDescription","noRowsAction","onOpen","loading","hideFooter","rowCount","onPageChange","onPageSizeChange","open","onClose","title","includeDescription","onSubmit","errorSlot","mt"],"mappings":"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"}
|