@aglyn/plugins-forms 1.0.0-beta.199 → 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.
@@ -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 readCampaignIds,\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 useHostCampaigns,\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, AGL-3330): every\n * column the table shows, typed, and the stored fields a site sorts its\n * forms out by, as columns kept out of sight.\n *\n * EVERY ONE NARROWS THE LOADED WINDOW; none is served. The list is a paged\n * listener over the document id (`collectionPage`), so a filter matches over\n * what the window has read, which the first filter widens\n * (`usePagedRowsFilter`) and the caption under the chips owns up to. The\n * query never changes shape, so no filter here needs an index. The figures\n * are the `stats` counters `/api/forms/submit` keeps on the form document\n * itself, read with the row.\n *\n * - Submissions and Leads are numbers. Leads is ABSENT on a form that does\n * not route to leads — the column's dash — so `is empty` finds those and\n * `= 0` finds a routed form that has produced none.\n * - Last submission, Updated and Created are days. Created is a column\n * hidden by default: `/api/hosts/resources` stamps it on every form it\n * makes.\n * - Status (active or retired), Lead routing (`routing.lead`) and Campaign\n * (`campaignIds`) are hidden columns, matched on keys derived by\n * {@link formFilterRow} so the row keeps the stored fields its menu reads.\n * A form's campaigns are ids, matched member by member and byte for byte,\n * as the campaigns list matches its `listIds`.\n *\n * Nothing else a form stores is a filter. It keeps no type and no template,\n * and its plan slot is not its own: a retired form keeps one, which is the\n * Status filter's answer.\n */\nexport const FORM_FILTER_FIELDS = [\n inMemoryListField('displayName', 'text'),\n inMemoryListField('slug', 'text'),\n inMemoryListField('submissions', 'number', 'stats.submissions'),\n inMemoryListField('leads', 'number', 'stats.leads'),\n inMemoryListField('lastSubmission', 'date', 'stats.lastSubmissionAtMs'),\n inMemoryListField('updatedAt', 'date'),\n inMemoryListField('createdAt', 'date'),\n inMemoryListField('status', 'select', 'statusKey'),\n inMemoryListField('leadRouting', 'select', 'leadRoutingKey'),\n {\n ...inMemoryListField('campaignIds', 'select', 'campaignKeys'),\n tokensPath: 'campaignKeys',\n verbatimTokens: true,\n },\n]\nexport const FORM_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 createdAt: 'Created',\n status: 'Status',\n leadRouting: 'Lead routing',\n campaignIds: 'Campaign',\n}\nconst FORM_STATUS_OPTIONS = [\n { value: 'active', label: 'Active' },\n { value: 'retired', label: 'Retired' },\n]\nconst FORM_LEAD_ROUTING_OPTIONS = [\n { value: 'on', label: 'On' },\n { value: 'off', label: 'Off' },\n]\n/** What the quick search reads: the name, the slug and the form's id. */\nexport const FORM_SEARCH_FIELDS = ['displayName', 'slug', '$id'] as const\nconst FORM_HIDDEN_COLUMNS = {\n createdAt: false,\n status: false,\n leadRouting: false,\n campaignIds: false,\n}\n\n/** A form row with the keys its hidden filters match on. */\nexport const formFilterRow = (form: any) => ({\n ...form,\n statusKey: isFormArchived(form) ? 'retired' : 'active',\n leadRoutingKey: form?.routing?.lead === true ? 'on' : 'off',\n campaignKeys: readCampaignIds(form),\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(formFilterRow),\n [formWindow, askedStatus],\n )\n const formWindowRows = useMemo(\n () =>\n formData\n ?.filter((form: any) => askedStatus || !isFormArchived(form))\n .map(formFilterRow),\n [formData, askedStatus],\n )\n /*\n * The Campaign filter's choices: the campaigns placed on this site, by\n * name, and any campaign a loaded form is filed under that the site's list\n * does not name — one since taken off this site — by its id, so the form\n * filed under it can still be found.\n *\n * Read only once a loaded form IS filed under a campaign. The names cost\n * the org lookup and up to fifty campaign documents, which a catalog with\n * no filed form would pay on every visit for a filter with nothing to find;\n * there the column has no choices and the panel leaves it out.\n */\n const filedCampaignIds = useMemo(\n () => [...new Set((formData ?? []).flatMap((form: any) => readCampaignIds(form)))],\n [formData],\n )\n const siteCampaigns = useHostCampaigns(hostId, {\n enabled: filedCampaignIds.length > 0,\n })\n const formFilterOptions = useMemo(() => {\n const named = siteCampaigns.options.map((option) => ({\n value: option.value,\n label: option.label,\n }))\n const unnamed = siteCampaigns.ready\n ? filedCampaignIds\n .filter((id) => !named.some((option) => option.value === id))\n .map((id) => ({ value: id, label: id }))\n : []\n return {\n status: FORM_STATUS_OPTIONS,\n leadRouting: FORM_LEAD_ROUTING_OPTIONS,\n campaignIds: [...named, ...unnamed],\n }\n }, [siteCampaigns.options, siteCampaigns.ready, filedCampaignIds])\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: formFilterOptions,\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 {\n // Hidden until Columns shows it; filterable either way (AGL-3330).\n field: 'createdAt',\n headerName: 'Created',\n minWidth: 170,\n flex: 1,\n type: 'date',\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","readCampaignIds","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","useHostCampaigns","useHostResourceApi","useLiveArtifactCount","usePagedCollection","collectionPage","useRouter","useCallback","useMemo","useState","BUNDLE_ID","FORM_FILTER_FIELDS","tokensPath","verbatimTokens","FORM_FILTER_HEADERS","displayName","slug","submissions","leads","lastSubmission","updatedAt","createdAt","status","leadRouting","campaignIds","FORM_STATUS_OPTIONS","value","label","FORM_LEAD_ROUTING_OPTIONS","FORM_SEARCH_FIELDS","FORM_HIDDEN_COLUMNS","formFilterRow","form","statusKey","leadRoutingKey","routing","lead","campaignKeys","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","filedCampaignIds","Set","flatMap","siteCampaigns","enabled","length","formFilterOptions","named","options","option","unnamed","ready","formFilter","fields","headers","search","clauses","onChange","forms","liveFormCount","formsUsed","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","toLocaleString","lastSubmissionAtMs","toDate","versionId","quick","icon","path","to","FORM_PREVIEW","unavailableReason","items","key","size","FORM_BESIGNER","onClick","request","name","dialog","direction","spacing","sx","alignItems","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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"}