@aglyn/plugins-forms 1.0.0-beta.217 → 1.0.0-beta.219
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 +14 -13
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/components/form-design-preview.component.d.ts +1 -1
- package/src/lib/components/form-design-preview.component.js +1 -1
- package/src/lib/components/form-design-preview.component.js.map +1 -1
- package/src/lib/components/form-detail-card.d.ts +1 -1
- package/src/lib/components/form-detail-card.js +5 -3
- package/src/lib/components/form-detail-card.js.map +1 -1
- package/src/lib/components/form.js +3 -3
- package/src/lib/components/form.js.map +1 -1
- package/src/lib/components/host-forms-card.component.js +7 -5
- package/src/lib/components/host-forms-card.component.js.map +1 -1
- package/src/lib/components/use-form-promote-api.d.ts +1 -1
- package/src/lib/components/use-form-promote-api.js +2 -2
- package/src/lib/components/use-form-promote-api.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +7 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/model/forms-record-routes.d.ts +22 -0
- package/src/lib/model/forms-record-routes.js +53 -0
- package/src/lib/model/forms-record-routes.js.map +1 -0
- package/src/lib/plan-entitlements.d.ts +41 -0
- package/src/lib/plan-entitlements.js +53 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +2 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/form-promote-route.d.ts +74 -0
- package/src/lib/server/form-promote-route.js +239 -0
- package/src/lib/server/form-promote-route.js.map +1 -0
- package/src/lib/server/form-promotion.d.ts +82 -0
- package/src/lib/server/form-promotion.js +133 -0
- package/src/lib/server/form-promotion.js.map +1 -0
- package/src/lib/server/form-publish-announce.d.ts +63 -0
- package/src/lib/server/form-publish-announce.js +142 -0
- package/src/lib/server/form-publish-announce.js.map +1 -0
- package/src/lib/server.d.ts +1 -0
- package/src/lib/server.js +6 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/usage-axes.d.ts +28 -0
- package/src/lib/usage-axes.js +52 -0
- package/src/lib/usage-axes.js.map +1 -0
|
@@ -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 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 { Timestamp } from '@aglyn/shared-util-timestamp'\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_IN_CAMPAIGN_OPTIONS,\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, In a 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, and recounted\n * from their rows after a removal (AGL-3330), so both are real numbers.\n * `leads` is counted only for a form whose `routing.lead` is set — it holds\n * `0` there until the first lead — so a form that has never routed to leads\n * carries no figure at all and renders as a dash rather than as `0`. A zero\n * would say this form has produced no leads, which for an unrouted form is a\n * 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 // Retiring and restoring are edits, and Updated says when the form\n // was last edited (AGL-3330): a write that skipped it left a form\n // retired today reading as untouched for weeks.\n updatedAt: Timestamp.now(),\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 inCampaign: FORM_IN_CAMPAIGN_OPTIONS,\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 holds no number.\n *\n * `stats.leads` is counted only for a form whose `routing.lead` is set\n * (`0` there until its first lead), so a form that has never routed to\n * leads has no recorded figure. Rendering `0` there would read as \"this\n * form has produced no leads\", which is a claim about a measurement\n * 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 /*\n * In a campaign, hidden like the two above. Reads the ids the form is\n * filed under — the fact `inCampaign` mirrors for the query — so a form\n * the backfill has not reached still draws the truth.\n */\n {\n field: 'inCampaign',\n headerName: 'In a campaign',\n minWidth: 130,\n valueGetter: (_value: any, row: any) =>\n String(Aglyn.readCampaignIds(row).length > 0),\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 // The rows keep the query's order; a header sort would order only\n // the page on screen and read as the whole list's.\n disableColumnSorting\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","Timestamp","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_IN_CAMPAIGN_OPTIONS","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","updatedAt","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","inCampaign","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","readCampaignIds","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","disableColumnSorting","rowCount","onPageChange","onPageSizeChange","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 { Timestamp } from '@aglyn/shared-util-timestamp'\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, FORMS_DOCUMENT_SEGMENT } from '../constants/bundle-common'\nimport {\n FORM_IN_CAMPAIGN_OPTIONS,\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, In a 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, and recounted\n * from their rows after a removal (AGL-3330), so both are real numbers.\n * `leads` is counted only for a form whose `routing.lead` is set — it holds\n * `0` there until the first lead — so a form that has never routed to leads\n * carries no figure at all and renders as a dash rather than as `0`. A zero\n * would say this form has produced no leads, which for an unrouted form is a\n * 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 // Retiring and restoring are edits, and Updated says when the form\n // was last edited (AGL-3330): a write that skipped it left a form\n // retired today reading as untouched for weeks.\n updatedAt: Timestamp.now(),\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 inCampaign: FORM_IN_CAMPAIGN_OPTIONS,\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 holds no number.\n *\n * `stats.leads` is counted only for a form whose `routing.lead` is set\n * (`0` there until its first lead), so a form that has never routed to\n * leads has no recorded figure. Rendering `0` there would read as \"this\n * form has produced no leads\", which is a claim about a measurement\n * 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 /*\n * In a campaign, hidden like the two above. Reads the ids the form is\n * filed under — the fact `inCampaign` mirrors for the query — so a form\n * the backfill has not reached still draws the truth.\n */\n {\n field: 'inCampaign',\n headerName: 'In a campaign',\n minWidth: 130,\n valueGetter: (_value: any, row: any) =>\n String(Aglyn.readCampaignIds(row).length > 0),\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.PLUGIN_DOCUMENT_PREVIEW, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: 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.PLUGIN_DOCUMENT_BESIGNER, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: 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 // The rows keep the query's order; a header sort would order only\n // the page on screen and read as the whole list's.\n disableColumnSorting\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","Timestamp","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","FORMS_DOCUMENT_SEGMENT","FORM_IN_CAMPAIGN_OPTIONS","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","updatedAt","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","inCampaign","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","readCampaignIds","form","versionId","quick","icon","path","to","PLUGIN_DOCUMENT_PREVIEW","documentSegment","docId","unavailableReason","items","key","size","PLUGIN_DOCUMENT_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","disableColumnSorting","rowCount","onPageChange","onPageSizeChange","open","onClose","title","includeDescription","onSubmit","errorSlot","mt"],"mappings":"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"}
|
|
@@ -34,7 +34,7 @@ export type PromoteForm = (options: {
|
|
|
34
34
|
* Makes one version of a form the version the site serves.
|
|
35
35
|
*
|
|
36
36
|
* Server-side, and never a client `updateDoc`, because promotion is where the
|
|
37
|
-
* form's contract is enforced: `/api/
|
|
37
|
+
* form's contract is enforced: `/api/forms/promote` re-reads the stored
|
|
38
38
|
* version, runs `checkFormContract` on the tree it is about to write, and
|
|
39
39
|
* refuses with a 422 rather than publishing a design whose submissions would
|
|
40
40
|
* silently stop arriving. A console-side check is advice; that route is the
|
|
@@ -21,7 +21,7 @@ import { useCallback } from "react";
|
|
|
21
21
|
* Makes one version of a form the version the site serves.
|
|
22
22
|
*
|
|
23
23
|
* Server-side, and never a client `updateDoc`, because promotion is where the
|
|
24
|
-
* form's contract is enforced: `/api/
|
|
24
|
+
* form's contract is enforced: `/api/forms/promote` re-reads the stored
|
|
25
25
|
* version, runs `checkFormContract` on the tree it is about to write, and
|
|
26
26
|
* refuses with a 422 rather than publishing a design whose submissions would
|
|
27
27
|
* silently stop arriving. A console-side check is advice; that route is the
|
|
@@ -34,7 +34,7 @@ import { useCallback } from "react";
|
|
|
34
34
|
const { data: user } = useUser();
|
|
35
35
|
return useCallback(async ({ hostId, formId, versionId })=>{
|
|
36
36
|
var _ref;
|
|
37
|
-
const response = await authorizedFetch(user, '/api/
|
|
37
|
+
const response = await authorizedFetch(user, '/api/forms/promote', {
|
|
38
38
|
method: 'POST',
|
|
39
39
|
headers: {
|
|
40
40
|
'Content-Type': 'application/json'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/use-form-promote-api.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'use client'\n\nimport type { FormContractViolation } from '@aglyn/aglyn'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useCallback } from 'react'\n\n/**\n * What the route refused with, or what it did.\n *\n * ONE shape rather than a discriminated union: `strictNullChecks` is off in\n * this repo, so narrowing on an `ok: true | false` discriminant does not\n * happen and every read of `message` would need a cast. A caller reads\n * `message` and `violations` only after seeing `ok === false`, and both are\n * always populated on a refusal — `violations` as an empty array when the\n * refusal was not about the contract at all (a role denial, a lockdown, a\n * version with no design).\n */\nexport interface PromoteFormResult {\n ok: boolean\n /**\n * Author-facing, and stated by the ROUTE. Present on a refusal.\n */\n message?: string\n /**\n * The contract violations, verbatim.\n *\n * The codes and sentences are `checkFormContract`'s own, so the page renders\n * exactly what the besigner renders and neither side parses prose.\n */\n violations?: FormContractViolation[]\n}\n\n/** Promote one version of one form, by id — no design crosses the wire. */\nexport type PromoteForm = (options: {\n hostId: string\n formId: string\n versionId: string\n}) => Promise<PromoteFormResult>\n\n/**\n * Makes one version of a form the version the site serves.\n *\n * Server-side, and never a client `updateDoc`, because promotion is where the\n * form's contract is enforced: `/api/
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/use-form-promote-api.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'use client'\n\nimport type { FormContractViolation } from '@aglyn/aglyn'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useCallback } from 'react'\n\n/**\n * What the route refused with, or what it did.\n *\n * ONE shape rather than a discriminated union: `strictNullChecks` is off in\n * this repo, so narrowing on an `ok: true | false` discriminant does not\n * happen and every read of `message` would need a cast. A caller reads\n * `message` and `violations` only after seeing `ok === false`, and both are\n * always populated on a refusal — `violations` as an empty array when the\n * refusal was not about the contract at all (a role denial, a lockdown, a\n * version with no design).\n */\nexport interface PromoteFormResult {\n ok: boolean\n /**\n * Author-facing, and stated by the ROUTE. Present on a refusal.\n */\n message?: string\n /**\n * The contract violations, verbatim.\n *\n * The codes and sentences are `checkFormContract`'s own, so the page renders\n * exactly what the besigner renders and neither side parses prose.\n */\n violations?: FormContractViolation[]\n}\n\n/** Promote one version of one form, by id — no design crosses the wire. */\nexport type PromoteForm = (options: {\n hostId: string\n formId: string\n versionId: string\n}) => Promise<PromoteFormResult>\n\n/**\n * Makes one version of a form the version the site serves.\n *\n * Server-side, and never a client `updateDoc`, because promotion is where the\n * form's contract is enforced: `/api/forms/promote` re-reads the stored\n * version, runs `checkFormContract` on the tree it is about to write, and\n * refuses with a 422 rather than publishing a design whose submissions would\n * silently stop arriving. A console-side check is advice; that route is the\n * enforcement, so this hook sends three ids and no design at all.\n *\n * Resolves rather than throws on a refusal: a broken contract is an ordinary\n * outcome an author is expected to see and fix, and the violations are the\n * payload, not an error message.\n */\nexport function useFormPromoteApi(): PromoteForm {\n const { data: user } = useUser()\n return useCallback<PromoteForm>(\n async ({ hostId, formId, versionId }) => {\n const response = await authorizedFetch(user, '/api/forms/promote', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, formId, versionId }),\n })\n const payload = await response.json().catch(() => ({}))\n if (response.ok) return { ok: true }\n return {\n ok: false,\n message: String(payload?.error ?? 'Publish failed'),\n violations: Array.isArray(payload?.violations)\n ? (payload.violations as FormContractViolation[])\n : [],\n }\n },\n [user],\n )\n}\n\nexport default useFormPromoteApi\n"],"names":["useUser","authorizedFetch","useCallback","useFormPromoteApi","data","user","hostId","formId","versionId","response","method","headers","body","JSON","stringify","payload","json","catch","ok","message","String","error","violations","Array","isArray"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAGA,SAASA,OAAO,QAAQ,iCAAgC;AACxD,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,WAAW,QAAQ,QAAO;AAmCnC;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC;IACd,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGL;IACvB,OAAOE,YACL,OAAO,EAAEI,MAAM,EAAEC,MAAM,EAAEC,SAAS,EAAE;;QAClC,MAAMC,WAAW,MAAMR,gBAAgBI,MAAM,sBAAsB;YACjEK,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CC,MAAMC,KAAKC,SAAS,CAAC;gBAAER;gBAAQC;gBAAQC;YAAU;QACnD;QACA,MAAMO,UAAU,MAAMN,SAASO,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;QACpD,IAAIR,SAASS,EAAE,EAAE,OAAO;YAAEA,IAAI;QAAK;QACnC,OAAO;YACLA,IAAI;YACJC,SAASC,eAAOL,2BAAAA,QAASM,KAAK,mBAAI;YAClCC,YAAYC,MAAMC,OAAO,CAACT,2BAAAA,QAASO,UAAU,IACxCP,QAAQO,UAAU,GACnB,EAAE;QACR;IACF,GACA;QAACjB;KAAK;AAEV;AAEA,eAAeF,kBAAiB"}
|
|
@@ -29,3 +29,11 @@
|
|
|
29
29
|
* agreement across a bundle move.
|
|
30
30
|
*/
|
|
31
31
|
export declare const BUNDLE_ID = "forms";
|
|
32
|
+
/**
|
|
33
|
+
* The segment a form's pages live under on a site, as `plugins.config.json`
|
|
34
|
+
* declares it in `besignerDocuments`: the form's own page is the hub's
|
|
35
|
+
* `forms/<formId>`, and the console's plugin-document editor and preview
|
|
36
|
+
* serve `forms/<formId>/versions/<versionId>/…`. In URLs already shared, so
|
|
37
|
+
* never renamed.
|
|
38
|
+
*/
|
|
39
|
+
export declare const FORMS_DOCUMENT_SEGMENT = "forms";
|
|
@@ -27,5 +27,12 @@
|
|
|
27
27
|
* `tools/scripts/backfill-node-plugin-ids.mjs` is what keeps the two in
|
|
28
28
|
* agreement across a bundle move.
|
|
29
29
|
*/ export const BUNDLE_ID = 'forms';
|
|
30
|
+
/**
|
|
31
|
+
* The segment a form's pages live under on a site, as `plugins.config.json`
|
|
32
|
+
* declares it in `besignerDocuments`: the form's own page is the hub's
|
|
33
|
+
* `forms/<formId>`, and the console's plugin-document editor and preview
|
|
34
|
+
* serve `forms/<formId>/versions/<versionId>/…`. In URLs already shared, so
|
|
35
|
+
* never renamed.
|
|
36
|
+
*/ export const FORMS_DOCUMENT_SEGMENT = 'forms';
|
|
30
37
|
|
|
31
38
|
//# sourceMappingURL=bundle-common.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/constants/bundle-common.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\n/**\n * The bundle id, persisted as `pluginId` on every node this bundle places.\n *\n * It is not a label. `requiredSitePlugins` reads `pluginId` off each saved\n * node to decide which bundles must register before first paint, so this\n * string is the answer to \"which chunk does this page need in front of the\n * render\". A node whose `pluginId` names a bundle that no longer registers\n * its `componentId` still RESOLVES — resolution is by component id alone —\n * but it resolves a beat late, after the post-hydration load of the rest of\n * the enabled set.\n *\n * `tools/scripts/backfill-node-plugin-ids.mjs` is what keeps the two in\n * agreement across a bundle move.\n */\nexport const BUNDLE_ID = 'forms'\n"],"names":["BUNDLE_ID"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMA,YAAY,QAAO"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/constants/bundle-common.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\n/**\n * The bundle id, persisted as `pluginId` on every node this bundle places.\n *\n * It is not a label. `requiredSitePlugins` reads `pluginId` off each saved\n * node to decide which bundles must register before first paint, so this\n * string is the answer to \"which chunk does this page need in front of the\n * render\". A node whose `pluginId` names a bundle that no longer registers\n * its `componentId` still RESOLVES — resolution is by component id alone —\n * but it resolves a beat late, after the post-hydration load of the rest of\n * the enabled set.\n *\n * `tools/scripts/backfill-node-plugin-ids.mjs` is what keeps the two in\n * agreement across a bundle move.\n */\nexport const BUNDLE_ID = 'forms'\n\n/**\n * The segment a form's pages live under on a site, as `plugins.config.json`\n * declares it in `besignerDocuments`: the form's own page is the hub's\n * `forms/<formId>`, and the console's plugin-document editor and preview\n * serve `forms/<formId>/versions/<versionId>/…`. In URLs already shared, so\n * never renamed.\n */\nexport const FORMS_DOCUMENT_SEGMENT = 'forms'\n"],"names":["BUNDLE_ID","FORMS_DOCUMENT_SEGMENT"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMA,YAAY,QAAO;AAEhC;;;;;;CAMC,GACD,OAAO,MAAMC,yBAAyB,QAAO"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Called from the console registrar. The owner is named rather than read off
|
|
19
|
+
* the loader's marker, so the route registers the same way under a spec that
|
|
20
|
+
* calls the registrar directly.
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerFormsRecordRoutes(): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
|
|
17
|
+
import { registerPluginRecordRoute } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
18
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
19
|
+
/**
|
|
20
|
+
* Where a form is read, published for every other surface.
|
|
21
|
+
*
|
|
22
|
+
* A campaign lists the forms filed under it, the CRM's lead routing names the
|
|
23
|
+
* forms that capture leads, and an assistant job opens the form it drafted.
|
|
24
|
+
* Each links to the form's own page, which is this plugin's — so they ask the
|
|
25
|
+
* record-route registry for `form` rather than spelling this plugin's nav
|
|
26
|
+
* slug, and get `null` (text instead of a link) where it is not loaded.
|
|
27
|
+
*
|
|
28
|
+
* A form belongs to one site: the organization level has no address for it.
|
|
29
|
+
*/ /** The nav slug the shell resolves this plugin's catalog by. */ const FORMS_SLUG = 'forms';
|
|
30
|
+
function catalog({ orgSlug, host }) {
|
|
31
|
+
return host ? buildRoute(Route.HOST_PLUGIN, {
|
|
32
|
+
orgSlug,
|
|
33
|
+
host,
|
|
34
|
+
pluginSlug: FORMS_SLUG
|
|
35
|
+
}) : null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Called from the console registrar. The owner is named rather than read off
|
|
39
|
+
* the loader's marker, so the route registers the same way under a spec that
|
|
40
|
+
* calls the registrar directly.
|
|
41
|
+
*/ export function registerFormsRecordRoutes() {
|
|
42
|
+
registerPluginRecordRoute('form', {
|
|
43
|
+
list: catalog,
|
|
44
|
+
record: (context, id)=>{
|
|
45
|
+
const list = catalog(context);
|
|
46
|
+
return list ? `${list}/${encodeURIComponent(id)}` : null;
|
|
47
|
+
}
|
|
48
|
+
}, {
|
|
49
|
+
pluginId: BUNDLE_ID
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=forms-record-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/model/forms-record-routes.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 { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * Where a form is read, published for every other surface.\n *\n * A campaign lists the forms filed under it, the CRM's lead routing names the\n * forms that capture leads, and an assistant job opens the form it drafted.\n * Each links to the form's own page, which is this plugin's — so they ask the\n * record-route registry for `form` rather than spelling this plugin's nav\n * slug, and get `null` (text instead of a link) where it is not loaded.\n *\n * A form belongs to one site: the organization level has no address for it.\n */\n\n/** The nav slug the shell resolves this plugin's catalog by. */\nconst FORMS_SLUG = 'forms'\n\nfunction catalog({ orgSlug, host }: PluginRecordRouteContext): string | null {\n return host ? buildRoute(Route.HOST_PLUGIN, { orgSlug, host, pluginSlug: FORMS_SLUG }) : null\n}\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the route registers the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerFormsRecordRoutes(): void {\n registerPluginRecordRoute(\n 'form',\n {\n list: catalog,\n record: (context, id) => {\n const list = catalog(context)\n return list ? `${list}/${encodeURIComponent(id)}` : null\n },\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","FORMS_SLUG","catalog","orgSlug","host","HOST_PLUGIN","pluginSlug","registerFormsRecordRoutes","list","record","context","id","encodeURIComponent","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;;;CAUC,GAED,8DAA8D,GAC9D,MAAMC,aAAa;AAEnB,SAASC,QAAQ,EAAEC,OAAO,EAAEC,IAAI,EAA4B;IAC1D,OAAOA,OAAOP,WAAWC,MAAMO,WAAW,EAAE;QAAEF;QAASC;QAAME,YAAYL;IAAW,KAAK;AAC3F;AAEA;;;;CAIC,GACD,OAAO,SAASM;IACdR,0BACE,QACA;QACES,MAAMN;QACNO,QAAQ,CAACC,SAASC;YAChB,MAAMH,OAAON,QAAQQ;YACrB,OAAOF,OAAO,GAAGA,KAAK,CAAC,EAAEI,mBAAmBD,KAAK,GAAG;QACtD;IACF,GACA;QAAEE,UAAUb;IAAU;AAE1B"}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types';
|
|
18
|
+
import type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements';
|
|
19
|
+
/**
|
|
20
|
+
* THE FORMS PLUGIN'S PLAN BAND (AGL-3080): the form submissions each plan
|
|
21
|
+
* includes a month, per site. Compiled into core's `PLAN_ENTITLEMENTS` by the
|
|
22
|
+
* manifest generator (`register.planEntitlements`); this is where the figure
|
|
23
|
+
* is written, beside the reasoning that sized it.
|
|
24
|
+
*/
|
|
25
|
+
declare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {
|
|
26
|
+
interface PluginEntitlementQuotas {
|
|
27
|
+
/**
|
|
28
|
+
* Form submissions accepted per calendar month, per site (Forms & Lead
|
|
29
|
+
* Capture). The org-wide band is this times `hostLimit`.
|
|
30
|
+
*
|
|
31
|
+
* Not the same dimension as `formsPerHost`, which counts the saved form
|
|
32
|
+
* CATALOG rather than the replies: only this one is tiered, metered and
|
|
33
|
+
* part of a charged price.
|
|
34
|
+
*/
|
|
35
|
+
formSubmissionsPerMonth?: number;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** The form submissions each plan includes a month, per site. */
|
|
39
|
+
export declare const FORM_SUBMISSIONS_PER_MONTH_BY_PLAN: Readonly<Record<OrgPlan, number>>;
|
|
40
|
+
/** The forms plugin's plan figures, for the manifest generator. */
|
|
41
|
+
export declare function formsPlanEntitlements(): PluginPlanEntitlementsDeclaration;
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
*/ // Types only: the manifest generator loads this module on its own to compile
|
|
17
|
+
// the plan figures, so nothing here may reach a core module at runtime.
|
|
18
|
+
/** The form submissions each plan includes a month, per site. */ export const FORM_SUBMISSIONS_PER_MONTH_BY_PLAN = {
|
|
19
|
+
// A `Form` node placed on a page needs no saved definition to collect, so
|
|
20
|
+
// Free — which has no form catalog — still spends this band on its replies.
|
|
21
|
+
free: 20,
|
|
22
|
+
starter: 200,
|
|
23
|
+
pro: 1000,
|
|
24
|
+
business: 8000,
|
|
25
|
+
scale: 25000,
|
|
26
|
+
advanced: 40000,
|
|
27
|
+
// FINITE, where every other capacity row on this tier is unbounded. The
|
|
28
|
+
// band is multiplied by `hostLimit` to get the org-wide band, so at 100
|
|
29
|
+
// hosts an unbounded figure was not merely large — it made this tier's cost
|
|
30
|
+
// model unbounded, and an unbounded term reads as ZERO in any analysis that
|
|
31
|
+
// scores an absent band as nothing. That is how it stayed invisible: the
|
|
32
|
+
// biggest line item on the most expensive self-serve plan, contributing 0
|
|
33
|
+
// to every total.
|
|
34
|
+
//
|
|
35
|
+
// Metering makes it safe to bound. `meteredInfraPassThrough` is true here,
|
|
36
|
+
// so submissions past the band BILL at the pass-through rate rather than
|
|
37
|
+
// being refused — a merchant's lead form does not stop working at 25,000.
|
|
38
|
+
agency: 25000,
|
|
39
|
+
enterprise: 50000
|
|
40
|
+
};
|
|
41
|
+
/** The forms plugin's plan figures, for the manifest generator. */ export function formsPlanEntitlements() {
|
|
42
|
+
return {
|
|
43
|
+
quotas: [
|
|
44
|
+
{
|
|
45
|
+
key: 'formSubmissionsPerMonth',
|
|
46
|
+
label: 'Form subs / mo',
|
|
47
|
+
byPlan: FORM_SUBMISSIONS_PER_MONTH_BY_PLAN
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=plan-entitlements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/forms/src/lib/plan-entitlements.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\n// Types only: the manifest generator loads this module on its own to compile\n// the plan figures, so nothing here may reach a core module at runtime.\nimport type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements'\n\n/**\n * THE FORMS PLUGIN'S PLAN BAND (AGL-3080): the form submissions each plan\n * includes a month, per site. Compiled into core's `PLAN_ENTITLEMENTS` by the\n * manifest generator (`register.planEntitlements`); this is where the figure\n * is written, beside the reasoning that sized it.\n */\ndeclare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {\n interface PluginEntitlementQuotas {\n /**\n * Form submissions accepted per calendar month, per site (Forms & Lead\n * Capture). The org-wide band is this times `hostLimit`.\n *\n * Not the same dimension as `formsPerHost`, which counts the saved form\n * CATALOG rather than the replies: only this one is tiered, metered and\n * part of a charged price.\n */\n formSubmissionsPerMonth?: number\n }\n}\n\n/** The form submissions each plan includes a month, per site. */\nexport const FORM_SUBMISSIONS_PER_MONTH_BY_PLAN: Readonly<Record<OrgPlan, number>> = {\n // A `Form` node placed on a page needs no saved definition to collect, so\n // Free — which has no form catalog — still spends this band on its replies.\n free: 20,\n starter: 200,\n pro: 1000,\n business: 8000,\n scale: 25000,\n advanced: 40000,\n // FINITE, where every other capacity row on this tier is unbounded. The\n // band is multiplied by `hostLimit` to get the org-wide band, so at 100\n // hosts an unbounded figure was not merely large — it made this tier's cost\n // model unbounded, and an unbounded term reads as ZERO in any analysis that\n // scores an absent band as nothing. That is how it stayed invisible: the\n // biggest line item on the most expensive self-serve plan, contributing 0\n // to every total.\n //\n // Metering makes it safe to bound. `meteredInfraPassThrough` is true here,\n // so submissions past the band BILL at the pass-through rate rather than\n // being refused — a merchant's lead form does not stop working at 25,000.\n agency: 25000,\n enterprise: 50_000,\n}\n\n/** The forms plugin's plan figures, for the manifest generator. */\nexport function formsPlanEntitlements(): PluginPlanEntitlementsDeclaration {\n return {\n quotas: [\n {\n key: 'formSubmissionsPerMonth',\n label: 'Form subs / mo',\n byPlan: FORM_SUBMISSIONS_PER_MONTH_BY_PLAN,\n },\n ],\n }\n}\n"],"names":["FORM_SUBMISSIONS_PER_MONTH_BY_PLAN","free","starter","pro","business","scale","advanced","agency","enterprise","formsPlanEntitlements","quotas","key","label","byPlan"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,wEAAwE;AAwBxE,+DAA+D,GAC/D,OAAO,MAAMA,qCAAwE;IACnF,0EAA0E;IAC1E,4EAA4E;IAC5EC,MAAM;IACNC,SAAS;IACTC,KAAK;IACLC,UAAU;IACVC,OAAO;IACPC,UAAU;IACV,wEAAwE;IACxE,wEAAwE;IACxE,4EAA4E;IAC5E,4EAA4E;IAC5E,yEAAyE;IACzE,0EAA0E;IAC1E,kBAAkB;IAClB,EAAE;IACF,2EAA2E;IAC3E,yEAAyE;IACzE,0EAA0E;IAC1EC,QAAQ;IACRC,YAAY;AACd,EAAC;AAED,iEAAiE,GACjE,OAAO,SAASC;IACd,OAAO;QACLC,QAAQ;YACN;gBACEC,KAAK;gBACLC,OAAO;gBACPC,QAAQb;YACV;SACD;IACH;AACF"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -19,6 +19,7 @@ import { lazy } from "react";
|
|
|
19
19
|
import { registerPluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
|
|
20
20
|
import { FORM_CONTACT_FIELDS_ZONE, FORM_SUBMISSIONS_ZONE } from "./components/form-zones.js";
|
|
21
21
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
22
|
+
import { registerFormsRecordRoutes } from "./model/forms-record-routes.js";
|
|
22
23
|
/** Code-split: the Forms console surface only loads when opened. */ const FormsConsolePage = lazy(()=>import("./components/forms-console-page.js"));
|
|
23
24
|
/**
|
|
24
25
|
* Console half: the Forms catalog and one form's own surface, served by the
|
|
@@ -27,6 +28,7 @@ import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
|
27
28
|
* `ownsSubtree` because a form's detail URL names a document id rather than a
|
|
28
29
|
* declared section. Safe to call at console app load — the page is lazy.
|
|
29
30
|
*/ export function registerFormsConsole() {
|
|
31
|
+
registerFormsRecordRoutes();
|
|
30
32
|
registerPluginZone({
|
|
31
33
|
zone: FORM_SUBMISSIONS_ZONE,
|
|
32
34
|
label: 'One form’s submissions',
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/forms/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiEmailFastOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport {\n FORM_CONTACT_FIELDS_ZONE,\n FORM_SUBMISSIONS_ZONE,\n} from './components/form-zones'\nimport { BUNDLE_ID } from './constants/bundle-common'\n\n/** Code-split: the Forms console surface only loads when opened. */\nconst FormsConsolePage = lazy(() => import('./components/forms-console-page'))\n\n/**\n * Console half: the Forms catalog and one form's own surface, served by the\n * shell's generic plugin route.\n *\n * `ownsSubtree` because a form's detail URL names a document id rather than a\n * declared section. Safe to call at console app load — the page is lazy.\n */\nexport function registerFormsConsole(): void {\n registerPluginZone(\n {\n zone: FORM_SUBMISSIONS_ZONE,\n label: 'One form’s submissions',\n surface: 'console',\n description:\n 'On one form’s page, behind the reader’s ask. A widget here reads the submissions to that form alone; it is handed the site and the form and nothing else.',\n },\n // Named, because a spec calls this registrar without the loader.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: FORM_CONTACT_FIELDS_ZONE,\n label: 'Where a form’s fields save on the person',\n surface: 'console',\n description:\n 'On one form’s page, beside routing and consent. A widget here decides which of the person’s fields each form field saves to and hands the new declaration to `saveFields`; the page writes the form document, and the widget writes nothing itself.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Forms',\n navItems: [\n {\n label: 'Forms',\n href: '/forms',\n // The tab id the console has always keyed this surface's active state\n // on. It carries no release flag: forms is on for every workspace, and\n // a site that switches it off loses this tab with the rest.\n navTabId: 'nav-tab-forms',\n icon: { path: mdiEmailFastOutline.path },\n ownsSubtree: true,\n header: {\n title: 'Forms',\n icon: { path: mdiEmailFastOutline.path },\n docsTopic: 'forms',\n },\n Component: FormsConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","mdiEmailFastOutline","lazy","registerPluginZone","FORM_CONTACT_FIELDS_ZONE","FORM_SUBMISSIONS_ZONE","BUNDLE_ID","FormsConsolePage","registerFormsConsole","zone","label","surface","description","pluginId","registerConsoleExtension","displayName","navItems","href","navTabId","icon","path","ownsSubtree","header","title","docsTopic","Component"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,mBAAmB,QAAQ,yBAAwB;AAC5D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SACEC,wBAAwB,EACxBC,qBAAqB,QAChB,6BAAyB;AAChC,SAASC,SAAS,QAAQ,+BAA2B;
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/forms/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiEmailFastOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport {\n FORM_CONTACT_FIELDS_ZONE,\n FORM_SUBMISSIONS_ZONE,\n} from './components/form-zones'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerFormsRecordRoutes } from './model/forms-record-routes'\n\n/** Code-split: the Forms console surface only loads when opened. */\nconst FormsConsolePage = lazy(() => import('./components/forms-console-page'))\n\n/**\n * Console half: the Forms catalog and one form's own surface, served by the\n * shell's generic plugin route.\n *\n * `ownsSubtree` because a form's detail URL names a document id rather than a\n * declared section. Safe to call at console app load — the page is lazy.\n */\nexport function registerFormsConsole(): void {\n registerFormsRecordRoutes()\n registerPluginZone(\n {\n zone: FORM_SUBMISSIONS_ZONE,\n label: 'One form’s submissions',\n surface: 'console',\n description:\n 'On one form’s page, behind the reader’s ask. A widget here reads the submissions to that form alone; it is handed the site and the form and nothing else.',\n },\n // Named, because a spec calls this registrar without the loader.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: FORM_CONTACT_FIELDS_ZONE,\n label: 'Where a form’s fields save on the person',\n surface: 'console',\n description:\n 'On one form’s page, beside routing and consent. A widget here decides which of the person’s fields each form field saves to and hands the new declaration to `saveFields`; the page writes the form document, and the widget writes nothing itself.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Forms',\n navItems: [\n {\n label: 'Forms',\n href: '/forms',\n // The tab id the console has always keyed this surface's active state\n // on. It carries no release flag: forms is on for every workspace, and\n // a site that switches it off loses this tab with the rest.\n navTabId: 'nav-tab-forms',\n icon: { path: mdiEmailFastOutline.path },\n ownsSubtree: true,\n header: {\n title: 'Forms',\n icon: { path: mdiEmailFastOutline.path },\n docsTopic: 'forms',\n },\n Component: FormsConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","mdiEmailFastOutline","lazy","registerPluginZone","FORM_CONTACT_FIELDS_ZONE","FORM_SUBMISSIONS_ZONE","BUNDLE_ID","registerFormsRecordRoutes","FormsConsolePage","registerFormsConsole","zone","label","surface","description","pluginId","registerConsoleExtension","displayName","navItems","href","navTabId","icon","path","ownsSubtree","header","title","docsTopic","Component"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,mBAAmB,QAAQ,yBAAwB;AAC5D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SACEC,wBAAwB,EACxBC,qBAAqB,QAChB,6BAAyB;AAChC,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,yBAAyB,QAAQ,iCAA6B;AAEvE,kEAAkE,GAClE,MAAMC,mBAAmBN,KAAK,IAAM,MAAM,CAAC;AAE3C;;;;;;CAMC,GACD,OAAO,SAASO;IACdF;IACAJ,mBACE;QACEO,MAAML;QACNM,OAAO;QACPC,SAAS;QACTC,aACE;IACJ,GACA,iEAAiE;IACjE;QAAEC,UAAUR;IAAU;IAExBH,mBACE;QACEO,MAAMN;QACNO,OAAO;QACPC,SAAS;QACTC,aACE;IACJ,GACA;QAAEC,UAAUR;IAAU;IAExBN,MAAMe,wBAAwB,CAAC;QAC7BD,UAAUR;QACVU,aAAa;QACbC,UAAU;YACR;gBACEN,OAAO;gBACPO,MAAM;gBACN,sEAAsE;gBACtE,uEAAuE;gBACvE,4DAA4D;gBAC5DC,UAAU;gBACVC,MAAM;oBAAEC,MAAMpB,oBAAoBoB,IAAI;gBAAC;gBACvCC,aAAa;gBACbC,QAAQ;oBACNC,OAAO;oBACPJ,MAAM;wBAAEC,MAAMpB,oBAAoBoB,IAAI;oBAAC;oBACvCI,WAAW;gBACb;gBACAC,WAAWlB;YACb;SACD;IACH;AACF;AAEA,cAAc,YAAQ"}
|