@aglyn/plugins-forms 1.0.0-beta.218 → 1.0.0-beta.220
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 -14
- 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 +12 -9
- package/src/lib/components/form-detail-card.js.map +1 -1
- package/src/lib/components/form-zones.d.ts +9 -0
- package/src/lib/components/form-zones.js +1 -0
- package/src/lib/components/form-zones.js.map +1 -1
- package/src/lib/components/form.js +5 -5
- package/src/lib/components/form.js.map +1 -1
- package/src/lib/components/host-forms-card.component.js +12 -9
- 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 +13 -1
- 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 +34 -0
- package/src/lib/usage-axes.js +68 -0
- package/src/lib/usage-axes.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-forms",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.220",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,25 +25,25 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-
|
|
34
|
-
"@aglyn/shared-
|
|
35
|
-
"@aglyn/shared-util-
|
|
36
|
-
"@aglyn/shared-util-
|
|
37
|
-
"@aglyn/
|
|
38
|
-
"@aglyn/tenant-
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.218",
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.220",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.220",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.220",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.220",
|
|
32
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.220",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.220",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.220",
|
|
35
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.220",
|
|
36
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.220",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.220",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.220",
|
|
40
39
|
"@swc/helpers": "0.5.23"
|
|
41
40
|
},
|
|
42
41
|
"peerDependencies": {
|
|
43
42
|
"@mui/material": "^9.3.1",
|
|
44
43
|
"@mui/x-data-grid": "^9.12.0",
|
|
45
44
|
"firebase": "^12.18.0",
|
|
46
|
-
"
|
|
45
|
+
"firebase-admin": "^14.3.0",
|
|
46
|
+
"next": "^16.3.3",
|
|
47
47
|
"react": "^19.2.8"
|
|
48
48
|
},
|
|
49
49
|
"sideEffects": false,
|
package/src/index.d.ts
CHANGED
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../libs/plugins/forms/src/index.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\nexport * from './lib/constants/bundle-common'\nexport * from './lib/plugin'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,mCAA+B;AAC7C,cAAc,kBAAc"}
|
|
1
|
+
{"version":3,"sources":["../../../../../libs/plugins/forms/src/index.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\nexport * from './lib/constants/bundle-common'\nexport * from './lib/plugin'\n// Type-only (AGL-3080): the plugin's entitlement keys are declared by module\n// augmentation in this module, so every program that loads the package\n// type-checks them. The figures reach core through the manifest generator.\nexport type { formsPlanEntitlements } from './lib/plan-entitlements'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,mCAA+B;AAC7C,cAAc,kBAAc"}
|
|
@@ -27,7 +27,7 @@ export declare function buildFormPreviewDocument(options: {
|
|
|
27
27
|
*
|
|
28
28
|
* A form has two halves and they fail differently. The DESIGN — fonts,
|
|
29
29
|
* spacing, the theme the site draws it in — is previewed by
|
|
30
|
-
* `Route.
|
|
30
|
+
* `Route.PLUGIN_DOCUMENT_PREVIEW`, which renders the stored nodes through
|
|
31
31
|
* `AglynNodeRenderer`, the same component tree the published page mounts. That
|
|
32
32
|
* is a whole-page surface and it stays where it is.
|
|
33
33
|
*
|
|
@@ -139,7 +139,7 @@ import { useMemo } from "react";
|
|
|
139
139
|
*
|
|
140
140
|
* A form has two halves and they fail differently. The DESIGN — fonts,
|
|
141
141
|
* spacing, the theme the site draws it in — is previewed by
|
|
142
|
-
* `Route.
|
|
142
|
+
* `Route.PLUGIN_DOCUMENT_PREVIEW`, which renders the stored nodes through
|
|
143
143
|
* `AglynNodeRenderer`, the same component tree the published page mounts. That
|
|
144
144
|
* is a whole-page surface and it stays where it is.
|
|
145
145
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-design-preview.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'\n// Every character that could close an attribute or open a tag, escaped by the\n// one implementation rather than a fifth copy of it. Subpath, not the library\n// index — see the note there.\nimport { escapeHtml } from '@aglyn/shared-util-tools/escape-html'\nimport { Box, Stack, Typography } from '@mui/material'\nimport { useMemo } from 'react'\n\nexport interface FormDesignPreviewProps {\n formId: string\n /** The form document's PUBLISHED `nodes` — what a visitor is served today. */\n nodes: unknown\n /** Which field the document names as the marketing opt-in. */\n consentFieldName?: string\n /** The document has not arrived yet, as against having no design. */\n loading?: boolean\n}\n\n/**\n * The `form` node inside a published design.\n *\n * A form document's tree holds exactly one, because the document IS that form\n * — but the tree is a flat map, so the node has to be found rather than\n * assumed to be the root.\n */\nfunction findFormNodeId(nodes: Record<string, any> | null): string | undefined {\n return Object.keys(nodes ?? {}).find(\n (id) => nodes?.[id]?.componentId === 'form',\n )\n}\n\n/**\n * One field, as the browser will submit it.\n *\n * The `name` attribute is the whole point: it is the key `/api/forms/submit`\n * reads out of the `FormData`, and it is the coupling `checkFormContract`\n * guards. Rendering the control WITH its name is what makes this a preview of\n * the contract rather than a picture of some inputs.\n */\nfunction renderField(field: Aglyn.FormFieldDecl, consentFieldName?: string): string {\n const name = escapeHtml(field.fieldName)\n const label = escapeHtml(field.label || field.fieldName)\n const required = field.required ? ' required' : ''\n const isConsent = Boolean(consentFieldName) && field.fieldName === consentFieldName\n const badge = isConsent\n ? '<span class=\"badge\">marketing consent</span>'\n : field.required\n ? '<span class=\"badge\">required</span>'\n : ''\n const options = (field.options ?? []).map((option) => escapeHtml(option))\n\n let control: string\n switch (field.fieldType) {\n case 'textarea':\n control = `<textarea name=\"${name}\" rows=\"3\"${required}></textarea>`\n break\n case 'select':\n control = `<select name=\"${name}\"${required}>${options\n .map((option) => `<option value=\"${option}\">${option}</option>`)\n .join('')}</select>`\n break\n case 'radio':\n control = `<div class=\"choices\">${options\n .map(\n (option) =>\n `<label class=\"choice\"><input type=\"radio\" name=\"${name}\" value=\"${option}\"${required}> ${option}</label>`,\n )\n .join('')}</div>`\n break\n case 'checkbox':\n control = `<label class=\"choice\"><input type=\"checkbox\" name=\"${name}\" value=\"on\"${required}> ${label}</label>`\n break\n case 'rating':\n // The runtime submits a number; a range says so without pretending to be\n // the star control the site draws.\n control = `<input type=\"range\" name=\"${name}\" min=\"1\" max=\"5\"${required}>`\n break\n case 'email':\n control = `<input type=\"email\" name=\"${name}\"${required}>`\n break\n default:\n control = `<input type=\"text\" name=\"${name}\"${required}>`\n }\n\n return [\n '<div class=\"field\">',\n `<div class=\"label\">${label}${badge}</div>`,\n control,\n `<div class=\"key\">name=\"${name}\"</div>`,\n '</div>',\n ].join('')\n}\n\n/** The style block, kept out of the builder so the markup reads as markup. */\nconst PREVIEW_CSS = `\n :root { color-scheme: light; }\n body {\n margin: 0; padding: 16px;\n font: 14px/1.5 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n color: #1a1a1a; background: #ffffff;\n }\n form { display: grid; gap: 16px; max-width: 520px; }\n .field { display: grid; gap: 4px; }\n .label { font-weight: 600; display: flex; align-items: center; gap: 8px; }\n .badge {\n font-weight: 500; font-size: 11px; letter-spacing: .04em;\n text-transform: uppercase; padding: 1px 6px; border-radius: 999px;\n background: #eceff4; color: #55606e;\n }\n .key { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px; color: #6b7280; }\n input[type=text], input[type=email], textarea, select {\n width: 100%; box-sizing: border-box; padding: 8px;\n border: 1px solid #c8cdd4; border-radius: 6px; font: inherit;\n background: #ffffff; color: inherit;\n }\n .choices { display: grid; gap: 4px; }\n .choice { font-weight: 400; display: flex; align-items: center; gap: 6px; }\n button {\n justify-self: start; padding: 8px 16px; border-radius: 6px;\n border: 1px solid #c8cdd4; background: #f4f6f8; font: inherit;\n }\n .none { color: #6b7280; }\n`\n\n/**\n * Builds the standalone document the frame is given.\n *\n * Exported for the spec: the property that matters — that no author-supplied\n * string can become markup — is a property of this string, and asserting it\n * through a rendered component would be asserting it through React's escaping\n * as well as this function's.\n */\nexport function buildFormPreviewDocument(options: {\n fields: Aglyn.FormFieldDecl[]\n consentFieldName?: string\n}): string {\n const { fields, consentFieldName } = options\n const body = fields.length\n ? fields.map((field) => renderField(field, consentFieldName)).join('')\n : '<p class=\"none\">This design declares no named fields.</p>'\n return [\n '<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">',\n '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">',\n `<style>${PREVIEW_CSS}</style></head><body>`,\n // `action` is deliberately absent and the frame is sandboxed, so this\n // cannot post anywhere even if a reader hits Return in a text field.\n `<form>${body}<button type=\"button\">Submit</button></form>`,\n '</body></html>',\n ].join('')\n}\n\n/**\n * THE FORM AS THE SUBMIT ROUTE WILL READ IT, DRAWN SAFELY.\n *\n * ## What is being previewed\n *\n * A form has two halves and they fail differently. The DESIGN — fonts,\n * spacing, the theme the site draws it in — is previewed by\n * `Route.FORM_PREVIEW`, which renders the stored nodes through\n * `AglynNodeRenderer`, the same component tree the published page mounts. That\n * is a whole-page surface and it stays where it is.\n *\n * This is the other half: the CONTRACT. `/api/forms/submit` never sees a\n * pixel. It sees `FormData` keys, and every coupling it depends on — which\n * field is the address a lead is created from, which field is the marketing\n * opt-in — is resolved by NAME. So the question this frame answers is the one\n * an author cannot answer by looking at the canvas: what names will arrive,\n * and which of them carry meaning.\n *\n * The field list is NOT re-derived here. `formFieldDeclsFromNodes` is the same\n * function the publish path calls to write the document's `fields` and the\n * same one adoption uses, including its two rules that are easy to get wrong\n * — an unnamed field is dropped, and a duplicate name keeps its first\n * occurrence — so this frame drops and keeps exactly what a real submission\n * does. A second implementation would be a preview of something else.\n *\n * ## Sandboxed, because every string in it is tenant-authored\n *\n * Labels, option text and field names are typed by a site's own editors, or\n * arrive on a marketplace template. They are escaped on the way into the\n * document AND the document is rendered into an iframe with an EMPTY `sandbox`\n * attribute — the maximally restrictive form: no scripts, no forms, no popups,\n * no top-level navigation and, critically, no same-origin. The frame gets an\n * opaque origin, so nothing inside it can reach the console's cookies, storage\n * or DOM. `srcDoc` rather than a URL keeps the markup from ever being served\n * from the console's own origin, where the sandbox attribute would be the only\n * thing between tenant content and a live session.\n *\n * The escaping and the sandbox are not redundant: the escaping is what keeps\n * the preview a preview of the author's text rather than of their markup, and\n * the sandbox is what makes being wrong about the escaping survivable.\n */\nexport function FormDesignPreview(props: FormDesignPreviewProps) {\n const { formId, nodes: rawNodes, consentFieldName, loading } = props\n\n const fields = useMemo(() => {\n const decoded = Aglyn.decodeStoredNodes<Record<string, any>>(rawNodes)\n if (!decoded || !Object.keys(decoded).length) return null\n const formNodeId = findFormNodeId(decoded)\n if (!formNodeId) return null\n return Aglyn.formFieldDeclsFromNodes(\n decoded as never,\n formNodeId as Aglyn.NodeId,\n )\n }, [rawNodes])\n\n const previewDocument = useMemo(\n () =>\n fields\n ? buildFormPreviewDocument({ fields, consentFieldName })\n : null,\n [fields, consentFieldName],\n )\n\n if (!previewDocument) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {loading\n ? 'Loading this form…'\n : 'Nothing published yet — open this form in the besigner, then ' +\n 'publish a version to see what it will collect.'}\n </Typography>\n )\n }\n\n return (\n <Stack spacing={1}>\n <Box\n component=\"iframe\"\n title={`Preview of form ${formId}`}\n /*\n * EMPTY sandbox — every restriction on, nothing allowed back. The\n * strings below are authored outside this console, so the document is\n * rendered with no scripts, no form submission, no navigation and an\n * opaque origin: it cannot reach the console's session, storage or DOM\n * even though it is drawn inside the console.\n */\n sandbox=\"\"\n referrerPolicy=\"no-referrer\"\n srcDoc={previewDocument}\n sx={{\n width: '100%',\n // Fixed, with the frame's own scrollbar. A sandbox with no scripts\n // is a frame that cannot measure or report its content height, and\n // an iframe cannot size to its content on its own.\n height: 420,\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n bgcolor: 'common.white',\n }}\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'These are the names a submission arrives under, read off the ' +\n 'published design by the same function that writes the form’s ' +\n 'declared fields. The site’s own styling is not applied here — ' +\n 'use Preview for that.'}\n </Typography>\n </Stack>\n )\n}\nFormDesignPreview.displayName = 'FormDesignPreview'\n\nexport default FormDesignPreview\n"],"names":["Aglyn","escapeHtml","Box","Stack","Typography","useMemo","findFormNodeId","nodes","Object","keys","find","id","componentId","renderField","field","consentFieldName","name","fieldName","label","required","isConsent","Boolean","badge","options","map","option","control","fieldType","join","PREVIEW_CSS","buildFormPreviewDocument","fields","body","length","FormDesignPreview","props","formId","rawNodes","loading","decoded","decodeStoredNodes","formNodeId","formFieldDeclsFromNodes","previewDocument","variant","color","spacing","component","title","sandbox","referrerPolicy","srcDoc","sx","width","height","border","borderColor","borderRadius","bgcolor","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-design-preview.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'\n// Every character that could close an attribute or open a tag, escaped by the\n// one implementation rather than a fifth copy of it. Subpath, not the library\n// index — see the note there.\nimport { escapeHtml } from '@aglyn/shared-util-tools/escape-html'\nimport { Box, Stack, Typography } from '@mui/material'\nimport { useMemo } from 'react'\n\nexport interface FormDesignPreviewProps {\n formId: string\n /** The form document's PUBLISHED `nodes` — what a visitor is served today. */\n nodes: unknown\n /** Which field the document names as the marketing opt-in. */\n consentFieldName?: string\n /** The document has not arrived yet, as against having no design. */\n loading?: boolean\n}\n\n/**\n * The `form` node inside a published design.\n *\n * A form document's tree holds exactly one, because the document IS that form\n * — but the tree is a flat map, so the node has to be found rather than\n * assumed to be the root.\n */\nfunction findFormNodeId(nodes: Record<string, any> | null): string | undefined {\n return Object.keys(nodes ?? {}).find(\n (id) => nodes?.[id]?.componentId === 'form',\n )\n}\n\n/**\n * One field, as the browser will submit it.\n *\n * The `name` attribute is the whole point: it is the key `/api/forms/submit`\n * reads out of the `FormData`, and it is the coupling `checkFormContract`\n * guards. Rendering the control WITH its name is what makes this a preview of\n * the contract rather than a picture of some inputs.\n */\nfunction renderField(field: Aglyn.FormFieldDecl, consentFieldName?: string): string {\n const name = escapeHtml(field.fieldName)\n const label = escapeHtml(field.label || field.fieldName)\n const required = field.required ? ' required' : ''\n const isConsent = Boolean(consentFieldName) && field.fieldName === consentFieldName\n const badge = isConsent\n ? '<span class=\"badge\">marketing consent</span>'\n : field.required\n ? '<span class=\"badge\">required</span>'\n : ''\n const options = (field.options ?? []).map((option) => escapeHtml(option))\n\n let control: string\n switch (field.fieldType) {\n case 'textarea':\n control = `<textarea name=\"${name}\" rows=\"3\"${required}></textarea>`\n break\n case 'select':\n control = `<select name=\"${name}\"${required}>${options\n .map((option) => `<option value=\"${option}\">${option}</option>`)\n .join('')}</select>`\n break\n case 'radio':\n control = `<div class=\"choices\">${options\n .map(\n (option) =>\n `<label class=\"choice\"><input type=\"radio\" name=\"${name}\" value=\"${option}\"${required}> ${option}</label>`,\n )\n .join('')}</div>`\n break\n case 'checkbox':\n control = `<label class=\"choice\"><input type=\"checkbox\" name=\"${name}\" value=\"on\"${required}> ${label}</label>`\n break\n case 'rating':\n // The runtime submits a number; a range says so without pretending to be\n // the star control the site draws.\n control = `<input type=\"range\" name=\"${name}\" min=\"1\" max=\"5\"${required}>`\n break\n case 'email':\n control = `<input type=\"email\" name=\"${name}\"${required}>`\n break\n default:\n control = `<input type=\"text\" name=\"${name}\"${required}>`\n }\n\n return [\n '<div class=\"field\">',\n `<div class=\"label\">${label}${badge}</div>`,\n control,\n `<div class=\"key\">name=\"${name}\"</div>`,\n '</div>',\n ].join('')\n}\n\n/** The style block, kept out of the builder so the markup reads as markup. */\nconst PREVIEW_CSS = `\n :root { color-scheme: light; }\n body {\n margin: 0; padding: 16px;\n font: 14px/1.5 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n color: #1a1a1a; background: #ffffff;\n }\n form { display: grid; gap: 16px; max-width: 520px; }\n .field { display: grid; gap: 4px; }\n .label { font-weight: 600; display: flex; align-items: center; gap: 8px; }\n .badge {\n font-weight: 500; font-size: 11px; letter-spacing: .04em;\n text-transform: uppercase; padding: 1px 6px; border-radius: 999px;\n background: #eceff4; color: #55606e;\n }\n .key { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px; color: #6b7280; }\n input[type=text], input[type=email], textarea, select {\n width: 100%; box-sizing: border-box; padding: 8px;\n border: 1px solid #c8cdd4; border-radius: 6px; font: inherit;\n background: #ffffff; color: inherit;\n }\n .choices { display: grid; gap: 4px; }\n .choice { font-weight: 400; display: flex; align-items: center; gap: 6px; }\n button {\n justify-self: start; padding: 8px 16px; border-radius: 6px;\n border: 1px solid #c8cdd4; background: #f4f6f8; font: inherit;\n }\n .none { color: #6b7280; }\n`\n\n/**\n * Builds the standalone document the frame is given.\n *\n * Exported for the spec: the property that matters — that no author-supplied\n * string can become markup — is a property of this string, and asserting it\n * through a rendered component would be asserting it through React's escaping\n * as well as this function's.\n */\nexport function buildFormPreviewDocument(options: {\n fields: Aglyn.FormFieldDecl[]\n consentFieldName?: string\n}): string {\n const { fields, consentFieldName } = options\n const body = fields.length\n ? fields.map((field) => renderField(field, consentFieldName)).join('')\n : '<p class=\"none\">This design declares no named fields.</p>'\n return [\n '<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">',\n '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">',\n `<style>${PREVIEW_CSS}</style></head><body>`,\n // `action` is deliberately absent and the frame is sandboxed, so this\n // cannot post anywhere even if a reader hits Return in a text field.\n `<form>${body}<button type=\"button\">Submit</button></form>`,\n '</body></html>',\n ].join('')\n}\n\n/**\n * THE FORM AS THE SUBMIT ROUTE WILL READ IT, DRAWN SAFELY.\n *\n * ## What is being previewed\n *\n * A form has two halves and they fail differently. The DESIGN — fonts,\n * spacing, the theme the site draws it in — is previewed by\n * `Route.PLUGIN_DOCUMENT_PREVIEW`, which renders the stored nodes through\n * `AglynNodeRenderer`, the same component tree the published page mounts. That\n * is a whole-page surface and it stays where it is.\n *\n * This is the other half: the CONTRACT. `/api/forms/submit` never sees a\n * pixel. It sees `FormData` keys, and every coupling it depends on — which\n * field is the address a lead is created from, which field is the marketing\n * opt-in — is resolved by NAME. So the question this frame answers is the one\n * an author cannot answer by looking at the canvas: what names will arrive,\n * and which of them carry meaning.\n *\n * The field list is NOT re-derived here. `formFieldDeclsFromNodes` is the same\n * function the publish path calls to write the document's `fields` and the\n * same one adoption uses, including its two rules that are easy to get wrong\n * — an unnamed field is dropped, and a duplicate name keeps its first\n * occurrence — so this frame drops and keeps exactly what a real submission\n * does. A second implementation would be a preview of something else.\n *\n * ## Sandboxed, because every string in it is tenant-authored\n *\n * Labels, option text and field names are typed by a site's own editors, or\n * arrive on a marketplace template. They are escaped on the way into the\n * document AND the document is rendered into an iframe with an EMPTY `sandbox`\n * attribute — the maximally restrictive form: no scripts, no forms, no popups,\n * no top-level navigation and, critically, no same-origin. The frame gets an\n * opaque origin, so nothing inside it can reach the console's cookies, storage\n * or DOM. `srcDoc` rather than a URL keeps the markup from ever being served\n * from the console's own origin, where the sandbox attribute would be the only\n * thing between tenant content and a live session.\n *\n * The escaping and the sandbox are not redundant: the escaping is what keeps\n * the preview a preview of the author's text rather than of their markup, and\n * the sandbox is what makes being wrong about the escaping survivable.\n */\nexport function FormDesignPreview(props: FormDesignPreviewProps) {\n const { formId, nodes: rawNodes, consentFieldName, loading } = props\n\n const fields = useMemo(() => {\n const decoded = Aglyn.decodeStoredNodes<Record<string, any>>(rawNodes)\n if (!decoded || !Object.keys(decoded).length) return null\n const formNodeId = findFormNodeId(decoded)\n if (!formNodeId) return null\n return Aglyn.formFieldDeclsFromNodes(\n decoded as never,\n formNodeId as Aglyn.NodeId,\n )\n }, [rawNodes])\n\n const previewDocument = useMemo(\n () =>\n fields\n ? buildFormPreviewDocument({ fields, consentFieldName })\n : null,\n [fields, consentFieldName],\n )\n\n if (!previewDocument) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {loading\n ? 'Loading this form…'\n : 'Nothing published yet — open this form in the besigner, then ' +\n 'publish a version to see what it will collect.'}\n </Typography>\n )\n }\n\n return (\n <Stack spacing={1}>\n <Box\n component=\"iframe\"\n title={`Preview of form ${formId}`}\n /*\n * EMPTY sandbox — every restriction on, nothing allowed back. The\n * strings below are authored outside this console, so the document is\n * rendered with no scripts, no form submission, no navigation and an\n * opaque origin: it cannot reach the console's session, storage or DOM\n * even though it is drawn inside the console.\n */\n sandbox=\"\"\n referrerPolicy=\"no-referrer\"\n srcDoc={previewDocument}\n sx={{\n width: '100%',\n // Fixed, with the frame's own scrollbar. A sandbox with no scripts\n // is a frame that cannot measure or report its content height, and\n // an iframe cannot size to its content on its own.\n height: 420,\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n bgcolor: 'common.white',\n }}\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'These are the names a submission arrives under, read off the ' +\n 'published design by the same function that writes the form’s ' +\n 'declared fields. The site’s own styling is not applied here — ' +\n 'use Preview for that.'}\n </Typography>\n </Stack>\n )\n}\nFormDesignPreview.displayName = 'FormDesignPreview'\n\nexport default FormDesignPreview\n"],"names":["Aglyn","escapeHtml","Box","Stack","Typography","useMemo","findFormNodeId","nodes","Object","keys","find","id","componentId","renderField","field","consentFieldName","name","fieldName","label","required","isConsent","Boolean","badge","options","map","option","control","fieldType","join","PREVIEW_CSS","buildFormPreviewDocument","fields","body","length","FormDesignPreview","props","formId","rawNodes","loading","decoded","decodeStoredNodes","formNodeId","formFieldDeclsFromNodes","previewDocument","variant","color","spacing","component","title","sandbox","referrerPolicy","srcDoc","sx","width","height","border","borderColor","borderRadius","bgcolor","displayName"],"mappings":"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"}
|
|
@@ -39,7 +39,7 @@ export interface FormDetailCardProps {
|
|
|
39
39
|
* how you go back, not only how you go forward, and an author restoring last
|
|
40
40
|
* week's design should not have to open a canvas to do it.
|
|
41
41
|
*
|
|
42
|
-
* It rides `/api/
|
|
42
|
+
* It rides `/api/forms/promote` rather than an `updateDoc` here, because
|
|
43
43
|
* a form's promotion has to run `checkFormContract` on the tree it is about to
|
|
44
44
|
* write and REFUSE. A check in this component would be advice a determined
|
|
45
45
|
* client could skip; the route reads the stored version itself, so nothing
|
|
@@ -30,12 +30,13 @@ import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.compon
|
|
|
30
30
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
31
31
|
import { Timestamp } from "@aglyn/shared-util-timestamp";
|
|
32
32
|
import { Alert, AlertTitle, Button, Chip, FormControlLabel, MenuItem, Stack, Switch, TableBody, TableCell, TableHead, TableRow, TextField, Typography } from "@mui/material";
|
|
33
|
-
import { useConsoleHostRoute, useFirestore, useFirestoreCollection, useFirestoreDoc,
|
|
34
|
-
import
|
|
33
|
+
import { useConsoleHostRoute, useFirestore, useFirestoreCollection, useFirestoreDoc, useSiteContainerOptions, useHostVersionApi } from "@aglyn/tenant-feature-instance";
|
|
34
|
+
import ContainerPicker from "@aglyn/tenant-feature-instance/components/container-picker";
|
|
35
35
|
import { collection, doc, limit, query, updateDoc } from "firebase/firestore";
|
|
36
36
|
import { useRouter } from "next/navigation";
|
|
37
37
|
import { useCallback, useMemo, useState } from "react";
|
|
38
38
|
import { FORM_CONTACT_FIELDS_ZONE } from "./form-zones.js";
|
|
39
|
+
import { FORMS_DOCUMENT_SEGMENT } from "../constants/bundle-common.js";
|
|
39
40
|
import FormDesignPreview from "./form-design-preview.component.js";
|
|
40
41
|
import FormMetricsCard from "./form-metrics-card.component.js";
|
|
41
42
|
import FormSubmissionsCard from "./form-submissions-card.component.js";
|
|
@@ -65,7 +66,7 @@ import useFormStatsRecountApi from "./use-form-stats-recount-api.js";
|
|
|
65
66
|
* how you go back, not only how you go forward, and an author restoring last
|
|
66
67
|
* week's design should not have to open a canvas to do it.
|
|
67
68
|
*
|
|
68
|
-
* It rides `/api/
|
|
69
|
+
* It rides `/api/forms/promote` rather than an `updateDoc` here, because
|
|
69
70
|
* a form's promotion has to run `checkFormContract` on the tree it is about to
|
|
70
71
|
* write and REFUSE. A check in this component would be advice a determined
|
|
71
72
|
* client could skip; the route reads the stored version itself, so nothing
|
|
@@ -188,10 +189,10 @@ import useFormStatsRecountApi from "./use-form-stats-recount-api.js";
|
|
|
188
189
|
* page that drew them until a button was pressed would be showing a
|
|
189
190
|
* merchant raw storage. Bounded at the ceiling the campaigns table itself
|
|
190
191
|
* draws, so it can offer nothing that list does not.
|
|
191
|
-
*/ const siteCampaigns =
|
|
192
|
+
*/ const siteCampaigns = useSiteContainerOptions('campaign', hostId, {
|
|
192
193
|
enabled: Boolean(form)
|
|
193
194
|
});
|
|
194
|
-
const storedCampaigns = useMemo(()=>Aglyn.
|
|
195
|
+
const storedCampaigns = useMemo(()=>Aglyn.readContainerIds(form, 'campaign'), [
|
|
195
196
|
form
|
|
196
197
|
]);
|
|
197
198
|
const effectiveCampaigns = campaignIds != null ? campaignIds : storedCampaigns;
|
|
@@ -396,10 +397,11 @@ import useFormStatsRecountApi from "./use-form-stats-recount-api.js";
|
|
|
396
397
|
// `useConsoleHostRoute` reads. Until they land the button below is
|
|
397
398
|
// disabled, so this branch cannot be reached without them.
|
|
398
399
|
if (orgSlug && host) {
|
|
399
|
-
router.push(buildRoute(Route.
|
|
400
|
+
router.push(buildRoute(Route.PLUGIN_DOCUMENT_BESIGNER, {
|
|
400
401
|
orgSlug,
|
|
401
402
|
host,
|
|
402
|
-
|
|
403
|
+
documentSegment: FORMS_DOCUMENT_SEGMENT,
|
|
404
|
+
docId: formId,
|
|
403
405
|
versionId
|
|
404
406
|
}));
|
|
405
407
|
}
|
|
@@ -425,7 +427,7 @@ import useFormStatsRecountApi from "./use-form-stats-recount-api.js";
|
|
|
425
427
|
host,
|
|
426
428
|
enqueueSnackbar
|
|
427
429
|
]);
|
|
428
|
-
const dirty = name != null || lead != null || consentField != null || campaignIds != null && !Aglyn.
|
|
430
|
+
const dirty = name != null || lead != null || consentField != null || campaignIds != null && !Aglyn.containerMembershipUnchanged(storedCampaigns, campaignIds);
|
|
429
431
|
// Named before it is used on every row, so a role denial reads as a reason
|
|
430
432
|
// rather than as a control that does nothing.
|
|
431
433
|
const publishBlock = hostRoleLoaded ? 'Your role on this site can edit content but not publish it' : 'Checking what your role can do…';
|
|
@@ -488,7 +490,8 @@ import useFormStatsRecountApi from "./use-form-stats-recount-api.js";
|
|
|
488
490
|
color: "text.secondary",
|
|
489
491
|
children: `ID ${formId} — every submission is filed under this, so it never changes`
|
|
490
492
|
}),
|
|
491
|
-
/*#__PURE__*/ _jsx(
|
|
493
|
+
/*#__PURE__*/ _jsx(ContainerPicker, {
|
|
494
|
+
kind: "campaign",
|
|
492
495
|
options: siteCampaigns.options,
|
|
493
496
|
value: effectiveCampaigns,
|
|
494
497
|
onChange: setCampaignIds,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-detail-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n buildRoute,\n type FormFieldDecl,\n formFieldsCanYieldAnEmail,\n formFieldsCaptureConsent,\n PageHeaderActions,\n PageHeaderRecord,\n listConsoleWidgets,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n// The CRM's route builder by its leaf path, not the plugin barrel: the barrel\n// is the entry point the tenant's loader imports to activate the plugin's\n// site half, and a console page named there would ship to every published\n// page. The same import the Inbox makes for the same reason.\nimport { pluginRecordFilteredHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { ICON_VARIANT_BESIGNER } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay, GridItems, MdiIcon, useLoading } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Alert,\n AlertTitle,\n Button,\n Chip,\n FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useHostCampaigns,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { collection, doc, limit, query, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { FORM_CONTACT_FIELDS_ZONE } from './form-zones'\nimport FormDesignPreview from './form-design-preview.component'\nimport FormMetricsCard from './form-metrics-card.component'\nimport FormSubmissionsCard from './form-submissions-card.component'\nimport useFormPromoteApi from './use-form-promote-api'\nimport useFormStatsRecountApi from './use-form-stats-recount-api'\n\nexport interface FormDetailCardProps {\n hostId: string\n formId: string\n /**\n * The Forms surface's own absolute console path.\n *\n * Only the not-found branch links it. The trail carries the way back on a\n * form that exists, so a link beside the heading would be the breadcrumb\n * written twice; a form that does NOT exist has no heading of its own for\n * the trail to end on, and the reader needs somewhere to go.\n */\n basePath?: string\n /** Whether this viewer's role on the site may make a version live. */\n canPublish?: boolean\n /** False while the role is still being read; see the disabled reasons. */\n hostRoleLoaded?: boolean\n}\n\n/**\n * One form — the component detail surface, for a document that is also a\n * contract.\n *\n * The split this enforces is the reason it exists rather than sending a row\n * straight into the besigner. A form has two halves:\n *\n * - the DESIGN, which is what an author draws, and which lives in the\n * besigner reached from here;\n * - the DECLARATION — where a submission is routed and which field carries\n * marketing consent — which is edited here.\n *\n * They are edited apart because the declaration is what the design is then\n * checked AGAINST. `checkFormContract` compares the two at publish, so a\n * surface that let an author change both in one motion would let them satisfy\n * the check by moving whichever side happened to be easier — which is how a\n * form ends up with lead routing pointed at a field nobody fills in.\n *\n * ## Promotion lives here, not only in the besigner\n *\n * A component's version history offers Publish on any version: promotion is\n * how you go back, not only how you go forward, and an author restoring last\n * week's design should not have to open a canvas to do it.\n *\n * It rides `/api/hosts/forms/promote` rather than an `updateDoc` here, because\n * a form's promotion has to run `checkFormContract` on the tree it is about to\n * write and REFUSE. A check in this component would be advice a determined\n * client could skip; the route reads the stored version itself, so nothing\n * about the design crosses the wire inbound and there is no version of this\n * surface that can publish a design the check has not seen.\n */\nexport function FormDetailCard(props: FormDetailCardProps) {\n const { hostId, formId, basePath, canPublish, hostRoleLoaded } = props\n const firestore = useFirestore()\n const ContactFieldsZone = useConsoleWidgetSlot()\n // Read at render, after the shell has loaded its plugins.\n const hasContactFieldsWidget =\n listConsoleWidgets(FORM_CONTACT_FIELDS_ZONE.id).length > 0\n /**\n * The write a widget in the contact-fields zone asks for. The declaration\n * is one array on the form document, and the document is this plugin's, so\n * the page writes what the widget decided.\n */\n const saveFields = useCallback(\n async (fields: FormFieldDecl[]) => {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n fields,\n updatedAt: Timestamp.now(),\n })\n },\n [firestore, hostId, formId],\n )\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const createHostVersion = useHostVersionApi()\n const promoteForm = useFormPromoteApi()\n const recountFormStats = useFormStatsRecountApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { queueLoading } = useLoading()\n\n const { data: form, status } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'forms', formId),\n [firestore, hostId, formId],\n { idField: '$id' },\n )\n // Three states, not two (AGL-706): loading and absent both arrive as\n // `undefined`, and an editable form over a mistyped id reads as data loss.\n const notFound = status !== 'loading' && !form\n\n /**\n * The published design, in BOTH stored forms (AGL-1151).\n *\n * The read above is converter-less, so `nodes` arrives exactly as stored —\n * msgpack for a design promoted since forms were compressed, a plain map\n * for everything older. Decoded once here because two surfaces below need\n * it and both fail silently on the raw value: `checkFormContract` reports\n * bogus violations against a tree it cannot walk, and the AGL-753 empty\n * guard passes on a `Bytes` because `Object.keys` counts the wrapper.\n */\n const formNodes = useMemo(\n () => Aglyn.decodeStoredNodes<Record<string, any>>(form?.nodes),\n [form?.nodes],\n )\n\n // No orderBy: the oldest version docs predate `createdAt`, and Firestore\n // drops documents missing the ordered field.\n const { data: versionDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms', formId, 'versions'),\n limit(100),\n ),\n [firestore, hostId, formId],\n { idField: '$id' },\n )\n const versions = [...(versionDocs ?? [])].sort(\n (a: any, b: any) =>\n (b.createdAt?.seconds ?? 0) - (a.createdAt?.seconds ?? 0),\n )\n const publishedVersionId = form?.versionId as string | undefined\n\n const [name, setName] = useState<string | null>(null)\n const [lead, setLead] = useState<boolean | null>(null)\n const [consentField, setConsentField] = useState<string | null>(null)\n /**\n * The campaigns picked on screen, or null while the stored value stands.\n *\n * Null and `[]` are different: null means \"not touched\", `[]` means \"take\n * this form out of every campaign\". Collapsing them would make clearing the\n * last campaign a no-op, which is the half of set-and-clear that is easy to\n * ship broken.\n */\n const [campaignIds, setCampaignIds] = useState<string[] | null>(null)\n const [opening, setOpening] = useState(false)\n const [promoting, setPromoting] = useState<string | null>(null)\n /**\n * What a REFUSED promotion said, kept on the surface rather than in a toast.\n *\n * A contract violation is a list of things to go and fix in the besigner,\n * and a snackbar is gone by the time the author has read the second one.\n * The besigner's own refusal persists for the same reason.\n */\n const [promoteRefusal, setPromoteRefusal] = useState<{\n message: string\n violations: Aglyn.FormContractViolation[]\n } | null>(null)\n\n /** The fields the PUBLISHED design draws, which is what a submission can carry. */\n const declaredFields: Aglyn.FormFieldDecl[] = useMemo(\n () => (Array.isArray(form?.fields) ? form.fields : []),\n [form],\n )\n\n const effectiveLead = lead ?? form?.routing?.lead === true\n const effectiveConsent = consentField ?? String(form?.consentFieldName ?? '')\n /*\n * Whether lead routing COULD work on this design — the two preconditions\n * `checkFormContract` applies at publish, asked here so the switch says so\n * before it is flipped rather than after (AGL-2612). Consent is read as\n * it is being edited: picking a field below is what satisfies it.\n */\n const hasEmailField = formFieldsCanYieldAnEmail(declaredFields)\n const capturesConsent = formFieldsCaptureConsent(declaredFields, effectiveConsent)\n /*\n * Where this form's people are: the contacts list narrowed to this form,\n * asked of the plugin that keeps people (`null`, and no link, without one). Built from the resolved org slug and\n * subdomain the besigner link already waits on, so the link appears when\n * they land and never points at a half-built address.\n */\n const contactsHref =\n orgSlug && host\n ? pluginRecordFilteredHref('contact', { orgSlug, host }, 'form', formId)\n : null\n /*\n * WHICH CAMPAIGNS THIS FORM IS PART OF.\n *\n * The site's campaigns are read while this page is open because the field\n * is DISPLAYED here as well as edited here: the document stores ids, and a\n * page that drew them until a button was pressed would be showing a\n * merchant raw storage. Bounded at the ceiling the campaigns table itself\n * draws, so it can offer nothing that list does not.\n */\n const siteCampaigns = useHostCampaigns(hostId, { enabled: Boolean(form) })\n const storedCampaigns = useMemo(\n () => Aglyn.readCampaignIds(form as Record<string, unknown>),\n [form],\n )\n const effectiveCampaigns = campaignIds ?? storedCampaigns\n\n /**\n * What the published design would break, judged against the declaration as\n * it is being edited here.\n *\n * Shown here as well as at publish because the two halves are edited in\n * different places: turning lead routing on for a form whose design has no\n * email field is a break the author causes HERE, on a design they are not\n * looking at, and the besigner would not tell them until they next opened\n * it.\n */\n const violations = useMemo(\n () =>\n form\n ? Aglyn.checkFormContract({\n form: {\n routing: { ...(form.routing ?? {}), lead: effectiveLead },\n ...(effectiveConsent\n ? { consentFieldName: effectiveConsent }\n : {}),\n },\n formId,\n nodes: formNodes as never,\n formNodeId: findFormNodeId(formNodes as never),\n })\n : [],\n [form, formId, effectiveLead, effectiveConsent],\n )\n\n const handleSave = useCallback(async () => {\n const dequeue = queueLoading()\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n // A rename carries the keys the Forms list searches by (AGL-3330), or\n // the form would be findable only by the name it used to have.\n ...(name != null\n ? {\n displayName: name.trim(),\n ...Aglyn.formListFields({\n id: formId,\n displayName: name.trim(),\n slug: form?.slug,\n }),\n }\n : {}),\n ...(lead != null ? { routing: { ...(form?.routing ?? {}), lead } } : {}),\n ...(consentField != null\n ? { consentFieldName: consentField.trim() }\n : {}),\n // An empty selection is stored as an empty array rather than removing\n // the field: one shape for \"in no campaign\", which is what keeps this\n // writer and the campaign's own detach agreeing. `inCampaign` rides\n // with it, the boolean the list's \"In a campaign\" filter asks\n // (AGL-3330).\n ...(campaignIds != null ? Aglyn.formCampaignFields(campaignIds) : {}),\n updatedAt: Timestamp.now(),\n })\n setName(null)\n setLead(null)\n setConsentField(null)\n setCampaignIds(null)\n enqueueSnackbar('Form saved', { variant: 'success', persist: false })\n /*\n * A form that routes leads holds `0` of them until its first, and one\n * that does not holds none (AGL-3330). The counters are the server's\n * to write, so the switch asks for them to be recounted.\n */\n if (lead != null && lead !== (form?.routing?.lead === true)) {\n void recountFormStats({ hostId, formIds: [formId] })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n }, [\n firestore,\n hostId,\n formId,\n form,\n name,\n lead,\n consentField,\n campaignIds,\n queueLoading,\n enqueueSnackbar,\n recountFormStats,\n ])\n\n /**\n * Makes one version the version the live sites serve.\n *\n * The route decides, not this handler: it re-reads the stored version, runs\n * `checkFormContract` on the tree it is about to write, and answers 422 with\n * the violations when a publish would stop the submissions arriving. So the\n * only thing here is what to do with each answer — and a refusal is put on\n * the page rather than in a toast, because it is a list of things to fix.\n */\n const handlePromote = useCallback(\n (targetVersionId: string) => async () => {\n if (promoting) return\n setPromoting(targetVersionId)\n setPromoteRefusal(null)\n try {\n const result = await promoteForm({ hostId, formId, versionId: targetVersionId })\n if (!result.ok) {\n const message = result.message ?? 'Publish failed'\n setPromoteRefusal({\n message,\n violations: result.violations ?? [],\n })\n // `persist` because this is a refusal an author has to act on: an\n // auto-dismissed warning is how someone walks away believing the\n // form shipped.\n enqueueSnackbar(message, {\n variant: 'warning',\n allowDuplicate: true,\n persist: true,\n })\n return\n }\n enqueueSnackbar(\n 'Published. The live sites now serve this version, and the form’s ' +\n 'declared fields match it.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Could not publish that version', {\n variant: 'error',\n allowDuplicate: true,\n persist: true,\n })\n } finally {\n setPromoting(null)\n }\n },\n [promoting, promoteForm, hostId, formId, enqueueSnackbar],\n )\n\n /**\n * Opens a version in the besigner, minting the first one when the form has\n * none.\n *\n * Forms adopted from a page carry a declared field list and a `nodes` map\n * seeded from the page they were adopted from — so the first version opens\n * on the form the site was ALREADY submitting rather than a blank canvas.\n */\n const handleOpen = useCallback(\n (targetVersionId?: string) => async () => {\n if (opening) return\n setOpening(true)\n try {\n let versionId = targetVersionId ?? publishedVersionId ?? versions[0]?.$id\n if (!versionId) {\n versionId = Aglyn.createResourceUid()\n // Rides /api/hosts/versions: rules deny the client create, and the\n // route allows a resource's FIRST version on every plan — which\n // this always is, since the form reached here with no `versionId`.\n await createHostVersion({\n hostId,\n kind: 'form',\n parentId: formId,\n id: versionId,\n data: {\n formId,\n hostId,\n displayName: 'Initial version',\n // Emptiness, not just absence: a form that holds no `nodes` and\n // `??` would pass an empty map straight through to a version\n // that opens uneditable (AGL-753). Measured on the DECODED\n // tree — `Object.keys` over a stored `Bytes` is non-empty, so\n // the raw value passes this guard and mints exactly the\n // uneditable version it exists to stop (AGL-1151).\n nodes: Object.keys(formNodes ?? {}).length\n ? formNodes\n : {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: [],\n },\n },\n ...(form?.rootId ? { rootId: form.rootId } : {}),\n },\n })\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n versionId,\n updatedAt: Timestamp.now(),\n })\n }\n // The editor's URL needs the resolved org slug and subdomain, which\n // `useConsoleHostRoute` reads. Until they land the button below is\n // disabled, so this branch cannot be reached without them.\n if (orgSlug && host) {\n router.push(\n buildRoute(Route.FORM_BESIGNER, { orgSlug, host, formId, versionId }),\n )\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Could not open the besigner', {\n variant: 'error',\n allowDuplicate: true,\n })\n setOpening(false)\n }\n },\n [\n opening,\n createHostVersion,\n publishedVersionId,\n versions,\n firestore,\n hostId,\n formId,\n form,\n router,\n orgSlug,\n host,\n enqueueSnackbar,\n ],\n )\n\n const dirty =\n name != null ||\n lead != null ||\n consentField != null ||\n (campaignIds != null &&\n !Aglyn.campaignMembershipUnchanged(storedCampaigns, campaignIds))\n // Named before it is used on every row, so a role denial reads as a reason\n // rather than as a control that does nothing.\n const publishBlock = hostRoleLoaded\n ? 'Your role on this site can edit content but not publish it'\n : 'Checking what your role can do…'\n\n if (notFound) {\n return (\n <CardDisplay\n header=\"Form not found\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'A form is created from the Forms list and keeps the id every ' +\n 'submission is filed under.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2} sx={{ alignItems: 'flex-start' }}>\n <Alert severity=\"info\">\n {`No form on this site has the id ${formId}. It may have been deleted, or the link may be from another site.`}\n </Alert>\n {basePath ? <AppLink href={basePath}>{'Back to forms'}</AppLink> : null}\n </Stack>\n </CardDisplay>\n )\n }\n\n /* The cards, named so the page chrome above them is a plain list of\n what this surface publishes upward. */\n const cards = (\n <GridItems\n spacing={3}\n items={[\n {\n size: { xs: 12, lg: 5 },\n children: (\n <CardDisplay\n header=\"Details\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'The id is what every submission is filed under, so ' +\n 'renaming a form never splits its history.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <TextField\n label=\"Display name\"\n size=\"small\"\n value={name ?? form?.displayName ?? ''}\n onChange={(event) => setName(event.target.value)}\n fullWidth\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`ID ${formId} — every submission is filed under this, so it never changes`}\n </Typography>\n {/*\n A FORM IS PART OF A CAMPAIGN, and may be part of several.\n The same signup form is placed by one push and re-placed by\n the next, so this is a multi-select — a single value would\n make filing it under this quarter's campaign silently\n un-file it from last quarter's.\n\n It is an assignment and nothing else: it does not decide who\n a campaign mails, and it does not credit a submission to\n one. Attribution is recorded from the link a visitor\n followed, on its own evidence, and it goes on saying what it\n says whatever is picked here.\n */}\n <CampaignPicker\n options={siteCampaigns.options}\n value={effectiveCampaigns}\n onChange={setCampaignIds}\n helperText=\"The campaigns this form is part of. It does not change who a campaign mails.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n />\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={!dirty}\n onClick={handleSave}\n >\n {'Save'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n ),\n },\n {\n size: { xs: 12, lg: 7 },\n children: (\n <CardDisplay\n header=\"CRM routing\"\n help={pluginDocsHelp('forms', {\n anchor: '#where-submissions-go',\n excerpt:\n 'Every submission reaches the Inbox and updates the ' +\n 'contact. Lead routing and the consent field decide ' +\n 'what else happens to it.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {/*\n THE PART THAT IS NOT A SWITCH (AGL-2612). Every capture\n lands in Contacts whatever is set below; the switch decides\n only whether a LEAD is filed beside the contact. Said\n first, because the switch on its own read as the only way\n a submission reached the CRM.\n */}\n <Typography variant=\"body2\">\n {'Every submission with an email address updates the person ' +\n 'in Contacts at stage Lead — a new contact when the ' +\n 'address is new, an interaction on their timeline when it ' +\n 'is not. A customer stays a customer.'}\n </Typography>\n <Stack spacing={0.5}>\n <FormControlLabel\n control={\n <Switch\n checked={effectiveLead}\n onChange={(event) => setLead(event.target.checked)}\n />\n }\n label=\"Also create a lead from the address someone gives this form\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {!hasEmailField\n ? 'Needs an email field in the published design — a ' +\n 'submission without an address cannot key a lead, and ' +\n 'publishing with this on would be refused until one exists.'\n : !capturesConsent\n ? 'Needs a marketing consent field — a lead nobody opted ' +\n 'in with is one the team cannot email, and publishing ' +\n 'with this on would be refused until the field below ' +\n 'names one.'\n : 'Files a lead in CRM → Leads for the sales team to work, ' +\n 'carrying the opt-in the consent field records.'}\n </Typography>\n </Stack>\n <TextField\n select\n label=\"Marketing consent field\"\n size=\"small\"\n value={effectiveConsent}\n onChange={(event) => setConsentField(event.target.value)}\n fullWidth\n helperText=\"The one field that IS the opt-in. Submitting a form is never itself consent.\"\n >\n <MenuItem value=\"\">\n {'None — this form collects no opt-in'}\n </MenuItem>\n {declaredFields.map((field) => (\n <MenuItem key={field.fieldName} value={field.fieldName}>\n {field.label\n ? `${field.label} (${field.fieldName})`\n : field.fieldName}\n </MenuItem>\n ))}\n </TextField>\n {violations.length ? (\n <Alert severity=\"warning\">\n <AlertTitle>{'This form would stop collecting'}</AlertTitle>\n <Stack component=\"ul\" sx={{ pl: 2, m: 0 }}>\n {violations.map((violation) => (\n <Typography\n component=\"li\"\n variant=\"body2\"\n key={`${violation.code}:${violation.nodeId ?? violation.fieldName ?? ''}`}\n >\n {violation.message}\n </Typography>\n ))}\n </Stack>\n </Alert>\n ) : null}\n {/*\n The people this form captured, on the list that holds them.\n Withheld until the site's address resolves, for the reason\n the besigner button is.\n */}\n {contactsHref ? (\n <Typography variant=\"body2\">\n <AppLink href={contactsHref}>\n {'See the contacts this form captured in the CRM'}\n </AppLink>\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n ),\n },\n // Where each field saves on the person: another plugin's card, in a\n // zone this page hosts. No item at all where nothing fills it, so the\n // grid keeps no empty cell.\n ...(ContactFieldsZone && hasContactFieldsWidget\n ? [\n {\n size: { xs: 12 },\n children: (\n <ContactFieldsZone\n slot={FORM_CONTACT_FIELDS_ZONE.id}\n hostId={hostId}\n formId={formId}\n fields={declaredFields}\n loading={status === 'loading'}\n saveFields={saveFields}\n />\n ),\n },\n ]\n : []),\n {\n size: { xs: 12, lg: 5 },\n children: (\n <FormMetricsCard\n stats={form?.stats}\n fields={declaredFields}\n leadRouting={effectiveLead}\n loading={status === 'loading'}\n />\n ),\n },\n {\n size: { xs: 12, lg: 7 },\n children: (\n <CardDisplay\n header=\"Versions\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'Publishing a form version updates every page that ' +\n 'places it. Saving does not — a save is not a publish.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {promoteRefusal ? (\n <Alert\n severity=\"warning\"\n onClose={() => setPromoteRefusal(null)}\n >\n <AlertTitle>{promoteRefusal.message}</AlertTitle>\n {promoteRefusal.violations.length ? (\n <Stack component=\"ul\" sx={{ pl: 2, m: 0 }}>\n {promoteRefusal.violations.map((violation) => (\n <Typography\n component=\"li\"\n variant=\"body2\"\n key={`${violation.code}:${violation.nodeId ?? violation.fieldName ?? ''}`}\n >\n {violation.message}\n </Typography>\n ))}\n </Stack>\n ) : null}\n </Alert>\n ) : null}\n {versions.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No versions yet — opening the besigner creates the first one.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Version'}</TableCell>\n <TableCell>{'Created'}</TableCell>\n <TableCell>{'Updated'}</TableCell>\n <TableCell align=\"right\">{'Actions'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {versions.map((version: any) => (\n <TableRow key={version.$id} hover>\n <TableCell>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"body2\">\n {version.displayName ?? version.$id}\n </Typography>\n {version.$id === publishedVersionId ? (\n <Chip\n label=\"Current\"\n color=\"success\"\n size=\"small\"\n />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n {version.createdAt?.toDate?.().toLocaleString() ??\n '--'}\n </TableCell>\n <TableCell>\n {version.updatedAt?.toDate?.().toLocaleString() ??\n '--'}\n </TableCell>\n <TableCell align=\"right\">\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n <Button\n size=\"small\"\n disabled={opening || !orgSlug || !host}\n onClick={handleOpen(version.$id)}\n >\n {'Open'}\n </Button>\n {/*\n The version the sites already serve has nothing\n to promote to, so the control is disabled\n rather than absent — an absent control and an\n inapplicable one look identical, and only one\n of them is honest.\n */}\n <Button\n size=\"small\"\n variant=\"outlined\"\n title={canPublish ? undefined : publishBlock}\n disabled={\n !canPublish ||\n promoting !== null ||\n version.$id === publishedVersionId\n }\n onClick={handlePromote(version.$id)}\n >\n {promoting === version.$id\n ? 'Publishing…'\n : version.$id === publishedVersionId\n ? 'Published'\n : 'Publish'}\n </Button>\n </Stack>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n )}\n </Stack>\n </CardDisplay>\n ),\n },\n {\n /*\n THE MESSAGES THEMSELVES, under the numbers that describe them.\n Full width because it is a table of people and what they wrote,\n and it sits below the metrics because those are read at a glance\n and this is read a row at a time.\n */\n size: { xs: 12 },\n children: <FormSubmissionsCard hostId={hostId} formId={formId} />,\n },\n {\n // Full width: the frame is a document, and a document in a\n // five-column card is a column of one-word lines.\n size: { xs: 12 },\n children: (\n <CardDisplay\n header=\"What a submission will carry\"\n help={pluginDocsHelp('forms', {\n anchor: '#field-types',\n excerpt:\n 'Each field arrives under its own name. Two fields ' +\n 'sharing one name arrive as a single answer.',\n })}\n contentGutterX\n contentGutterY\n >\n <FormDesignPreview\n formId={formId}\n nodes={form?.nodes}\n consentFieldName={\n effectiveConsent ? effectiveConsent : undefined\n }\n loading={status === 'loading'}\n />\n </CardDisplay>\n ),\n },\n ]}\n />\n )\n\n return (\n <>\n {/*\n The page chrome this surface cannot set for itself. The console shell\n owns the layout and builds its heading from the Forms nav item, so\n without these the page about one form is headed `Forms` and its trail\n stops on the list — the same on every row of it.\n */}\n <PageHeaderRecord\n title={form ? form.displayName || formId : undefined}\n />\n {/*\n The besigner is what this surface exists to reach, so it leads — in\n the PAGE header, which is where an action belongs on a surface with\n no section rail beneath it. Withheld when there is no form: it would\n mint a version document under an id that has none (AGL-706).\n */}\n <PageHeaderActions>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={opening || !orgSlug || !host}\n title={\n orgSlug && host ? undefined : 'Resolving this site’s address…'\n }\n onClick={handleOpen()}\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {opening ? 'Opening…' : 'Edit in besigner'}\n </Button>\n </PageHeaderActions>\n {cards}\n </>\n )\n}\nFormDetailCard.displayName = 'FormDetailCard'\n\n/**\n * The `form` node inside a published design.\n *\n * A form document's tree holds exactly one, because the document IS that\n * form — but the tree is a flat map with a synthetic root, so the node has to\n * be found rather than assumed to be the root itself.\n */\nfunction findFormNodeId(\n nodes: Record<string, { componentId?: string }> | undefined,\n): string | undefined {\n return Object.keys(nodes ?? {}).find(\n (id) => nodes?.[id]?.componentId === 'form',\n )\n}\n\nexport default FormDetailCard\n"],"names":["Aglyn","buildRoute","formFieldsCanYieldAnEmail","formFieldsCaptureConsent","PageHeaderActions","PageHeaderRecord","listConsoleWidgets","pluginDocsHelp","Route","useConsoleWidgetSlot","pluginRecordFilteredHref","ICON_VARIANT_BESIGNER","AppLink","CardDisplay","GridItems","MdiIcon","useLoading","ScrollTable","useSnackbar","Timestamp","Alert","AlertTitle","Button","Chip","FormControlLabel","MenuItem","Stack","Switch","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useConsoleHostRoute","useFirestore","useFirestoreCollection","useFirestoreDoc","useHostCampaigns","useHostVersionApi","CampaignPicker","collection","doc","limit","query","updateDoc","useRouter","useCallback","useMemo","useState","FORM_CONTACT_FIELDS_ZONE","FormDesignPreview","FormMetricsCard","FormSubmissionsCard","useFormPromoteApi","useFormStatsRecountApi","FormDetailCard","props","name","form","hostId","formId","basePath","canPublish","hostRoleLoaded","firestore","ContactFieldsZone","hasContactFieldsWidget","id","length","saveFields","fields","updatedAt","now","orgSlug","subdomain","host","createHostVersion","promoteForm","recountFormStats","router","enqueueSnackbar","queueLoading","data","status","idField","notFound","formNodes","decodeStoredNodes","nodes","versionDocs","versions","sort","a","b","createdAt","seconds","publishedVersionId","versionId","setName","lead","setLead","consentField","setConsentField","campaignIds","setCampaignIds","opening","setOpening","promoting","setPromoting","promoteRefusal","setPromoteRefusal","declaredFields","Array","isArray","effectiveLead","routing","effectiveConsent","String","consentFieldName","hasEmailField","capturesConsent","contactsHref","siteCampaigns","enabled","Boolean","storedCampaigns","readCampaignIds","effectiveCampaigns","violations","checkFormContract","formNodeId","findFormNodeId","handleSave","dequeue","displayName","trim","formListFields","slug","formCampaignFields","variant","persist","formIds","error","console","allowDuplicate","handlePromote","targetVersionId","result","ok","message","handleOpen","$id","createResourceUid","kind","parentId","Object","keys","CANVAS_ROOT_ELEMENT_ID","componentId","rootId","push","FORM_BESIGNER","dirty","campaignMembershipUnchanged","publishBlock","header","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","sx","alignItems","severity","href","cards","items","size","xs","lg","children","label","value","onChange","event","target","fullWidth","color","options","helperText","empty","ready","direction","disabled","onClick","control","checked","select","map","field","fieldName","component","pl","m","violation","code","nodeId","slot","loading","stats","leadRouting","onClose","align","version","hover","toDate","toLocaleString","justifyContent","title","undefined","startIcon","path","find"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-detail-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n buildRoute,\n type FormFieldDecl,\n formFieldsCanYieldAnEmail,\n formFieldsCaptureConsent,\n PageHeaderActions,\n PageHeaderRecord,\n listConsoleWidgets,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n// The CRM's route builder by its leaf path, not the plugin barrel: the barrel\n// is the entry point the tenant's loader imports to activate the plugin's\n// site half, and a console page named there would ship to every published\n// page. The same import the Inbox makes for the same reason.\nimport { pluginRecordFilteredHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { ICON_VARIANT_BESIGNER } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay, GridItems, MdiIcon, useLoading } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Alert,\n AlertTitle,\n Button,\n Chip,\n FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useSiteContainerOptions,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport ContainerPicker from '@aglyn/tenant-feature-instance/components/container-picker'\nimport { collection, doc, limit, query, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { FORM_CONTACT_FIELDS_ZONE } from './form-zones'\nimport { FORMS_DOCUMENT_SEGMENT } from '../constants/bundle-common'\nimport FormDesignPreview from './form-design-preview.component'\nimport FormMetricsCard from './form-metrics-card.component'\nimport FormSubmissionsCard from './form-submissions-card.component'\nimport useFormPromoteApi from './use-form-promote-api'\nimport useFormStatsRecountApi from './use-form-stats-recount-api'\n\nexport interface FormDetailCardProps {\n hostId: string\n formId: string\n /**\n * The Forms surface's own absolute console path.\n *\n * Only the not-found branch links it. The trail carries the way back on a\n * form that exists, so a link beside the heading would be the breadcrumb\n * written twice; a form that does NOT exist has no heading of its own for\n * the trail to end on, and the reader needs somewhere to go.\n */\n basePath?: string\n /** Whether this viewer's role on the site may make a version live. */\n canPublish?: boolean\n /** False while the role is still being read; see the disabled reasons. */\n hostRoleLoaded?: boolean\n}\n\n/**\n * One form — the component detail surface, for a document that is also a\n * contract.\n *\n * The split this enforces is the reason it exists rather than sending a row\n * straight into the besigner. A form has two halves:\n *\n * - the DESIGN, which is what an author draws, and which lives in the\n * besigner reached from here;\n * - the DECLARATION — where a submission is routed and which field carries\n * marketing consent — which is edited here.\n *\n * They are edited apart because the declaration is what the design is then\n * checked AGAINST. `checkFormContract` compares the two at publish, so a\n * surface that let an author change both in one motion would let them satisfy\n * the check by moving whichever side happened to be easier — which is how a\n * form ends up with lead routing pointed at a field nobody fills in.\n *\n * ## Promotion lives here, not only in the besigner\n *\n * A component's version history offers Publish on any version: promotion is\n * how you go back, not only how you go forward, and an author restoring last\n * week's design should not have to open a canvas to do it.\n *\n * It rides `/api/forms/promote` rather than an `updateDoc` here, because\n * a form's promotion has to run `checkFormContract` on the tree it is about to\n * write and REFUSE. A check in this component would be advice a determined\n * client could skip; the route reads the stored version itself, so nothing\n * about the design crosses the wire inbound and there is no version of this\n * surface that can publish a design the check has not seen.\n */\nexport function FormDetailCard(props: FormDetailCardProps) {\n const { hostId, formId, basePath, canPublish, hostRoleLoaded } = props\n const firestore = useFirestore()\n const ContactFieldsZone = useConsoleWidgetSlot()\n // Read at render, after the shell has loaded its plugins.\n const hasContactFieldsWidget =\n listConsoleWidgets(FORM_CONTACT_FIELDS_ZONE.id).length > 0\n /**\n * The write a widget in the contact-fields zone asks for. The declaration\n * is one array on the form document, and the document is this plugin's, so\n * the page writes what the widget decided.\n */\n const saveFields = useCallback(\n async (fields: FormFieldDecl[]) => {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n fields,\n updatedAt: Timestamp.now(),\n })\n },\n [firestore, hostId, formId],\n )\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const createHostVersion = useHostVersionApi()\n const promoteForm = useFormPromoteApi()\n const recountFormStats = useFormStatsRecountApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { queueLoading } = useLoading()\n\n const { data: form, status } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'forms', formId),\n [firestore, hostId, formId],\n { idField: '$id' },\n )\n // Three states, not two (AGL-706): loading and absent both arrive as\n // `undefined`, and an editable form over a mistyped id reads as data loss.\n const notFound = status !== 'loading' && !form\n\n /**\n * The published design, in BOTH stored forms (AGL-1151).\n *\n * The read above is converter-less, so `nodes` arrives exactly as stored —\n * msgpack for a design promoted since forms were compressed, a plain map\n * for everything older. Decoded once here because two surfaces below need\n * it and both fail silently on the raw value: `checkFormContract` reports\n * bogus violations against a tree it cannot walk, and the AGL-753 empty\n * guard passes on a `Bytes` because `Object.keys` counts the wrapper.\n */\n const formNodes = useMemo(\n () => Aglyn.decodeStoredNodes<Record<string, any>>(form?.nodes),\n [form?.nodes],\n )\n\n // No orderBy: the oldest version docs predate `createdAt`, and Firestore\n // drops documents missing the ordered field.\n const { data: versionDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms', formId, 'versions'),\n limit(100),\n ),\n [firestore, hostId, formId],\n { idField: '$id' },\n )\n const versions = [...(versionDocs ?? [])].sort(\n (a: any, b: any) =>\n (b.createdAt?.seconds ?? 0) - (a.createdAt?.seconds ?? 0),\n )\n const publishedVersionId = form?.versionId as string | undefined\n\n const [name, setName] = useState<string | null>(null)\n const [lead, setLead] = useState<boolean | null>(null)\n const [consentField, setConsentField] = useState<string | null>(null)\n /**\n * The campaigns picked on screen, or null while the stored value stands.\n *\n * Null and `[]` are different: null means \"not touched\", `[]` means \"take\n * this form out of every campaign\". Collapsing them would make clearing the\n * last campaign a no-op, which is the half of set-and-clear that is easy to\n * ship broken.\n */\n const [campaignIds, setCampaignIds] = useState<string[] | null>(null)\n const [opening, setOpening] = useState(false)\n const [promoting, setPromoting] = useState<string | null>(null)\n /**\n * What a REFUSED promotion said, kept on the surface rather than in a toast.\n *\n * A contract violation is a list of things to go and fix in the besigner,\n * and a snackbar is gone by the time the author has read the second one.\n * The besigner's own refusal persists for the same reason.\n */\n const [promoteRefusal, setPromoteRefusal] = useState<{\n message: string\n violations: Aglyn.FormContractViolation[]\n } | null>(null)\n\n /** The fields the PUBLISHED design draws, which is what a submission can carry. */\n const declaredFields: Aglyn.FormFieldDecl[] = useMemo(\n () => (Array.isArray(form?.fields) ? form.fields : []),\n [form],\n )\n\n const effectiveLead = lead ?? form?.routing?.lead === true\n const effectiveConsent = consentField ?? String(form?.consentFieldName ?? '')\n /*\n * Whether lead routing COULD work on this design — the two preconditions\n * `checkFormContract` applies at publish, asked here so the switch says so\n * before it is flipped rather than after (AGL-2612). Consent is read as\n * it is being edited: picking a field below is what satisfies it.\n */\n const hasEmailField = formFieldsCanYieldAnEmail(declaredFields)\n const capturesConsent = formFieldsCaptureConsent(declaredFields, effectiveConsent)\n /*\n * Where this form's people are: the contacts list narrowed to this form,\n * asked of the plugin that keeps people (`null`, and no link, without one). Built from the resolved org slug and\n * subdomain the besigner link already waits on, so the link appears when\n * they land and never points at a half-built address.\n */\n const contactsHref =\n orgSlug && host\n ? pluginRecordFilteredHref('contact', { orgSlug, host }, 'form', formId)\n : null\n /*\n * WHICH CAMPAIGNS THIS FORM IS PART OF.\n *\n * The site's campaigns are read while this page is open because the field\n * is DISPLAYED here as well as edited here: the document stores ids, and a\n * page that drew them until a button was pressed would be showing a\n * merchant raw storage. Bounded at the ceiling the campaigns table itself\n * draws, so it can offer nothing that list does not.\n */\n const siteCampaigns = useSiteContainerOptions('campaign', hostId, { enabled: Boolean(form) })\n const storedCampaigns = useMemo(\n () => Aglyn.readContainerIds(form as Record<string, unknown>, 'campaign'),\n [form],\n )\n const effectiveCampaigns = campaignIds ?? storedCampaigns\n\n /**\n * What the published design would break, judged against the declaration as\n * it is being edited here.\n *\n * Shown here as well as at publish because the two halves are edited in\n * different places: turning lead routing on for a form whose design has no\n * email field is a break the author causes HERE, on a design they are not\n * looking at, and the besigner would not tell them until they next opened\n * it.\n */\n const violations = useMemo(\n () =>\n form\n ? Aglyn.checkFormContract({\n form: {\n routing: { ...(form.routing ?? {}), lead: effectiveLead },\n ...(effectiveConsent\n ? { consentFieldName: effectiveConsent }\n : {}),\n },\n formId,\n nodes: formNodes as never,\n formNodeId: findFormNodeId(formNodes as never),\n })\n : [],\n [form, formId, effectiveLead, effectiveConsent],\n )\n\n const handleSave = useCallback(async () => {\n const dequeue = queueLoading()\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n // A rename carries the keys the Forms list searches by (AGL-3330), or\n // the form would be findable only by the name it used to have.\n ...(name != null\n ? {\n displayName: name.trim(),\n ...Aglyn.formListFields({\n id: formId,\n displayName: name.trim(),\n slug: form?.slug,\n }),\n }\n : {}),\n ...(lead != null ? { routing: { ...(form?.routing ?? {}), lead } } : {}),\n ...(consentField != null\n ? { consentFieldName: consentField.trim() }\n : {}),\n // An empty selection is stored as an empty array rather than removing\n // the field: one shape for \"in no campaign\", which is what keeps this\n // writer and the campaign's own detach agreeing. `inCampaign` rides\n // with it, the boolean the list's \"In a campaign\" filter asks\n // (AGL-3330).\n ...(campaignIds != null ? Aglyn.formCampaignFields(campaignIds) : {}),\n updatedAt: Timestamp.now(),\n })\n setName(null)\n setLead(null)\n setConsentField(null)\n setCampaignIds(null)\n enqueueSnackbar('Form saved', { variant: 'success', persist: false })\n /*\n * A form that routes leads holds `0` of them until its first, and one\n * that does not holds none (AGL-3330). The counters are the server's\n * to write, so the switch asks for them to be recounted.\n */\n if (lead != null && lead !== (form?.routing?.lead === true)) {\n void recountFormStats({ hostId, formIds: [formId] })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n }, [\n firestore,\n hostId,\n formId,\n form,\n name,\n lead,\n consentField,\n campaignIds,\n queueLoading,\n enqueueSnackbar,\n recountFormStats,\n ])\n\n /**\n * Makes one version the version the live sites serve.\n *\n * The route decides, not this handler: it re-reads the stored version, runs\n * `checkFormContract` on the tree it is about to write, and answers 422 with\n * the violations when a publish would stop the submissions arriving. So the\n * only thing here is what to do with each answer — and a refusal is put on\n * the page rather than in a toast, because it is a list of things to fix.\n */\n const handlePromote = useCallback(\n (targetVersionId: string) => async () => {\n if (promoting) return\n setPromoting(targetVersionId)\n setPromoteRefusal(null)\n try {\n const result = await promoteForm({ hostId, formId, versionId: targetVersionId })\n if (!result.ok) {\n const message = result.message ?? 'Publish failed'\n setPromoteRefusal({\n message,\n violations: result.violations ?? [],\n })\n // `persist` because this is a refusal an author has to act on: an\n // auto-dismissed warning is how someone walks away believing the\n // form shipped.\n enqueueSnackbar(message, {\n variant: 'warning',\n allowDuplicate: true,\n persist: true,\n })\n return\n }\n enqueueSnackbar(\n 'Published. The live sites now serve this version, and the form’s ' +\n 'declared fields match it.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Could not publish that version', {\n variant: 'error',\n allowDuplicate: true,\n persist: true,\n })\n } finally {\n setPromoting(null)\n }\n },\n [promoting, promoteForm, hostId, formId, enqueueSnackbar],\n )\n\n /**\n * Opens a version in the besigner, minting the first one when the form has\n * none.\n *\n * Forms adopted from a page carry a declared field list and a `nodes` map\n * seeded from the page they were adopted from — so the first version opens\n * on the form the site was ALREADY submitting rather than a blank canvas.\n */\n const handleOpen = useCallback(\n (targetVersionId?: string) => async () => {\n if (opening) return\n setOpening(true)\n try {\n let versionId = targetVersionId ?? publishedVersionId ?? versions[0]?.$id\n if (!versionId) {\n versionId = Aglyn.createResourceUid()\n // Rides /api/hosts/versions: rules deny the client create, and the\n // route allows a resource's FIRST version on every plan — which\n // this always is, since the form reached here with no `versionId`.\n await createHostVersion({\n hostId,\n kind: 'form',\n parentId: formId,\n id: versionId,\n data: {\n formId,\n hostId,\n displayName: 'Initial version',\n // Emptiness, not just absence: a form that holds no `nodes` and\n // `??` would pass an empty map straight through to a version\n // that opens uneditable (AGL-753). Measured on the DECODED\n // tree — `Object.keys` over a stored `Bytes` is non-empty, so\n // the raw value passes this guard and mints exactly the\n // uneditable version it exists to stop (AGL-1151).\n nodes: Object.keys(formNodes ?? {}).length\n ? formNodes\n : {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: [],\n },\n },\n ...(form?.rootId ? { rootId: form.rootId } : {}),\n },\n })\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n versionId,\n updatedAt: Timestamp.now(),\n })\n }\n // The editor's URL needs the resolved org slug and subdomain, which\n // `useConsoleHostRoute` reads. Until they land the button below is\n // disabled, so this branch cannot be reached without them.\n if (orgSlug && host) {\n router.push(\n buildRoute(Route.PLUGIN_DOCUMENT_BESIGNER, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: formId,\n versionId,\n }),\n )\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Could not open the besigner', {\n variant: 'error',\n allowDuplicate: true,\n })\n setOpening(false)\n }\n },\n [\n opening,\n createHostVersion,\n publishedVersionId,\n versions,\n firestore,\n hostId,\n formId,\n form,\n router,\n orgSlug,\n host,\n enqueueSnackbar,\n ],\n )\n\n const dirty =\n name != null ||\n lead != null ||\n consentField != null ||\n (campaignIds != null &&\n !Aglyn.containerMembershipUnchanged(storedCampaigns, campaignIds))\n // Named before it is used on every row, so a role denial reads as a reason\n // rather than as a control that does nothing.\n const publishBlock = hostRoleLoaded\n ? 'Your role on this site can edit content but not publish it'\n : 'Checking what your role can do…'\n\n if (notFound) {\n return (\n <CardDisplay\n header=\"Form not found\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'A form is created from the Forms list and keeps the id every ' +\n 'submission is filed under.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2} sx={{ alignItems: 'flex-start' }}>\n <Alert severity=\"info\">\n {`No form on this site has the id ${formId}. It may have been deleted, or the link may be from another site.`}\n </Alert>\n {basePath ? <AppLink href={basePath}>{'Back to forms'}</AppLink> : null}\n </Stack>\n </CardDisplay>\n )\n }\n\n /* The cards, named so the page chrome above them is a plain list of\n what this surface publishes upward. */\n const cards = (\n <GridItems\n spacing={3}\n items={[\n {\n size: { xs: 12, lg: 5 },\n children: (\n <CardDisplay\n header=\"Details\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'The id is what every submission is filed under, so ' +\n 'renaming a form never splits its history.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <TextField\n label=\"Display name\"\n size=\"small\"\n value={name ?? form?.displayName ?? ''}\n onChange={(event) => setName(event.target.value)}\n fullWidth\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`ID ${formId} — every submission is filed under this, so it never changes`}\n </Typography>\n {/*\n A FORM IS PART OF A CAMPAIGN, and may be part of several.\n The same signup form is placed by one push and re-placed by\n the next, so this is a multi-select — a single value would\n make filing it under this quarter's campaign silently\n un-file it from last quarter's.\n\n It is an assignment and nothing else: it does not decide who\n a campaign mails, and it does not credit a submission to\n one. Attribution is recorded from the link a visitor\n followed, on its own evidence, and it goes on saying what it\n says whatever is picked here.\n */}\n <ContainerPicker\n kind=\"campaign\"\n options={siteCampaigns.options}\n value={effectiveCampaigns}\n onChange={setCampaignIds}\n helperText=\"The campaigns this form is part of. It does not change who a campaign mails.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n />\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={!dirty}\n onClick={handleSave}\n >\n {'Save'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n ),\n },\n {\n size: { xs: 12, lg: 7 },\n children: (\n <CardDisplay\n header=\"CRM routing\"\n help={pluginDocsHelp('forms', {\n anchor: '#where-submissions-go',\n excerpt:\n 'Every submission reaches the Inbox and updates the ' +\n 'contact. Lead routing and the consent field decide ' +\n 'what else happens to it.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {/*\n THE PART THAT IS NOT A SWITCH (AGL-2612). Every capture\n lands in Contacts whatever is set below; the switch decides\n only whether a LEAD is filed beside the contact. Said\n first, because the switch on its own read as the only way\n a submission reached the CRM.\n */}\n <Typography variant=\"body2\">\n {'Every submission with an email address updates the person ' +\n 'in Contacts at stage Lead — a new contact when the ' +\n 'address is new, an interaction on their timeline when it ' +\n 'is not. A customer stays a customer.'}\n </Typography>\n <Stack spacing={0.5}>\n <FormControlLabel\n control={\n <Switch\n checked={effectiveLead}\n onChange={(event) => setLead(event.target.checked)}\n />\n }\n label=\"Also create a lead from the address someone gives this form\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {!hasEmailField\n ? 'Needs an email field in the published design — a ' +\n 'submission without an address cannot key a lead, and ' +\n 'publishing with this on would be refused until one exists.'\n : !capturesConsent\n ? 'Needs a marketing consent field — a lead nobody opted ' +\n 'in with is one the team cannot email, and publishing ' +\n 'with this on would be refused until the field below ' +\n 'names one.'\n : 'Files a lead in CRM → Leads for the sales team to work, ' +\n 'carrying the opt-in the consent field records.'}\n </Typography>\n </Stack>\n <TextField\n select\n label=\"Marketing consent field\"\n size=\"small\"\n value={effectiveConsent}\n onChange={(event) => setConsentField(event.target.value)}\n fullWidth\n helperText=\"The one field that IS the opt-in. Submitting a form is never itself consent.\"\n >\n <MenuItem value=\"\">\n {'None — this form collects no opt-in'}\n </MenuItem>\n {declaredFields.map((field) => (\n <MenuItem key={field.fieldName} value={field.fieldName}>\n {field.label\n ? `${field.label} (${field.fieldName})`\n : field.fieldName}\n </MenuItem>\n ))}\n </TextField>\n {violations.length ? (\n <Alert severity=\"warning\">\n <AlertTitle>{'This form would stop collecting'}</AlertTitle>\n <Stack component=\"ul\" sx={{ pl: 2, m: 0 }}>\n {violations.map((violation) => (\n <Typography\n component=\"li\"\n variant=\"body2\"\n key={`${violation.code}:${violation.nodeId ?? violation.fieldName ?? ''}`}\n >\n {violation.message}\n </Typography>\n ))}\n </Stack>\n </Alert>\n ) : null}\n {/*\n The people this form captured, on the list that holds them.\n Withheld until the site's address resolves, for the reason\n the besigner button is.\n */}\n {contactsHref ? (\n <Typography variant=\"body2\">\n <AppLink href={contactsHref}>\n {'See the contacts this form captured in the CRM'}\n </AppLink>\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n ),\n },\n // Where each field saves on the person: another plugin's card, in a\n // zone this page hosts. No item at all where nothing fills it, so the\n // grid keeps no empty cell.\n ...(ContactFieldsZone && hasContactFieldsWidget\n ? [\n {\n size: { xs: 12 },\n children: (\n <ContactFieldsZone\n slot={FORM_CONTACT_FIELDS_ZONE.id}\n hostId={hostId}\n formId={formId}\n fields={declaredFields}\n loading={status === 'loading'}\n saveFields={saveFields}\n />\n ),\n },\n ]\n : []),\n {\n size: { xs: 12, lg: 5 },\n children: (\n <FormMetricsCard\n stats={form?.stats}\n fields={declaredFields}\n leadRouting={effectiveLead}\n loading={status === 'loading'}\n />\n ),\n },\n {\n size: { xs: 12, lg: 7 },\n children: (\n <CardDisplay\n header=\"Versions\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'Publishing a form version updates every page that ' +\n 'places it. Saving does not — a save is not a publish.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {promoteRefusal ? (\n <Alert\n severity=\"warning\"\n onClose={() => setPromoteRefusal(null)}\n >\n <AlertTitle>{promoteRefusal.message}</AlertTitle>\n {promoteRefusal.violations.length ? (\n <Stack component=\"ul\" sx={{ pl: 2, m: 0 }}>\n {promoteRefusal.violations.map((violation) => (\n <Typography\n component=\"li\"\n variant=\"body2\"\n key={`${violation.code}:${violation.nodeId ?? violation.fieldName ?? ''}`}\n >\n {violation.message}\n </Typography>\n ))}\n </Stack>\n ) : null}\n </Alert>\n ) : null}\n {versions.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No versions yet — opening the besigner creates the first one.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Version'}</TableCell>\n <TableCell>{'Created'}</TableCell>\n <TableCell>{'Updated'}</TableCell>\n <TableCell align=\"right\">{'Actions'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {versions.map((version: any) => (\n <TableRow key={version.$id} hover>\n <TableCell>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"body2\">\n {version.displayName ?? version.$id}\n </Typography>\n {version.$id === publishedVersionId ? (\n <Chip\n label=\"Current\"\n color=\"success\"\n size=\"small\"\n />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n {version.createdAt?.toDate?.().toLocaleString() ??\n '--'}\n </TableCell>\n <TableCell>\n {version.updatedAt?.toDate?.().toLocaleString() ??\n '--'}\n </TableCell>\n <TableCell align=\"right\">\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n <Button\n size=\"small\"\n disabled={opening || !orgSlug || !host}\n onClick={handleOpen(version.$id)}\n >\n {'Open'}\n </Button>\n {/*\n The version the sites already serve has nothing\n to promote to, so the control is disabled\n rather than absent — an absent control and an\n inapplicable one look identical, and only one\n of them is honest.\n */}\n <Button\n size=\"small\"\n variant=\"outlined\"\n title={canPublish ? undefined : publishBlock}\n disabled={\n !canPublish ||\n promoting !== null ||\n version.$id === publishedVersionId\n }\n onClick={handlePromote(version.$id)}\n >\n {promoting === version.$id\n ? 'Publishing…'\n : version.$id === publishedVersionId\n ? 'Published'\n : 'Publish'}\n </Button>\n </Stack>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n )}\n </Stack>\n </CardDisplay>\n ),\n },\n {\n /*\n THE MESSAGES THEMSELVES, under the numbers that describe them.\n Full width because it is a table of people and what they wrote,\n and it sits below the metrics because those are read at a glance\n and this is read a row at a time.\n */\n size: { xs: 12 },\n children: <FormSubmissionsCard hostId={hostId} formId={formId} />,\n },\n {\n // Full width: the frame is a document, and a document in a\n // five-column card is a column of one-word lines.\n size: { xs: 12 },\n children: (\n <CardDisplay\n header=\"What a submission will carry\"\n help={pluginDocsHelp('forms', {\n anchor: '#field-types',\n excerpt:\n 'Each field arrives under its own name. Two fields ' +\n 'sharing one name arrive as a single answer.',\n })}\n contentGutterX\n contentGutterY\n >\n <FormDesignPreview\n formId={formId}\n nodes={form?.nodes}\n consentFieldName={\n effectiveConsent ? effectiveConsent : undefined\n }\n loading={status === 'loading'}\n />\n </CardDisplay>\n ),\n },\n ]}\n />\n )\n\n return (\n <>\n {/*\n The page chrome this surface cannot set for itself. The console shell\n owns the layout and builds its heading from the Forms nav item, so\n without these the page about one form is headed `Forms` and its trail\n stops on the list — the same on every row of it.\n */}\n <PageHeaderRecord\n title={form ? form.displayName || formId : undefined}\n />\n {/*\n The besigner is what this surface exists to reach, so it leads — in\n the PAGE header, which is where an action belongs on a surface with\n no section rail beneath it. Withheld when there is no form: it would\n mint a version document under an id that has none (AGL-706).\n */}\n <PageHeaderActions>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={opening || !orgSlug || !host}\n title={\n orgSlug && host ? undefined : 'Resolving this site’s address…'\n }\n onClick={handleOpen()}\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {opening ? 'Opening…' : 'Edit in besigner'}\n </Button>\n </PageHeaderActions>\n {cards}\n </>\n )\n}\nFormDetailCard.displayName = 'FormDetailCard'\n\n/**\n * The `form` node inside a published design.\n *\n * A form document's tree holds exactly one, because the document IS that\n * form — but the tree is a flat map with a synthetic root, so the node has to\n * be found rather than assumed to be the root itself.\n */\nfunction findFormNodeId(\n nodes: Record<string, { componentId?: string }> | undefined,\n): string | undefined {\n return Object.keys(nodes ?? {}).find(\n (id) => nodes?.[id]?.componentId === 'form',\n )\n}\n\nexport default FormDetailCard\n"],"names":["Aglyn","buildRoute","formFieldsCanYieldAnEmail","formFieldsCaptureConsent","PageHeaderActions","PageHeaderRecord","listConsoleWidgets","pluginDocsHelp","Route","useConsoleWidgetSlot","pluginRecordFilteredHref","ICON_VARIANT_BESIGNER","AppLink","CardDisplay","GridItems","MdiIcon","useLoading","ScrollTable","useSnackbar","Timestamp","Alert","AlertTitle","Button","Chip","FormControlLabel","MenuItem","Stack","Switch","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useConsoleHostRoute","useFirestore","useFirestoreCollection","useFirestoreDoc","useSiteContainerOptions","useHostVersionApi","ContainerPicker","collection","doc","limit","query","updateDoc","useRouter","useCallback","useMemo","useState","FORM_CONTACT_FIELDS_ZONE","FORMS_DOCUMENT_SEGMENT","FormDesignPreview","FormMetricsCard","FormSubmissionsCard","useFormPromoteApi","useFormStatsRecountApi","FormDetailCard","props","name","form","hostId","formId","basePath","canPublish","hostRoleLoaded","firestore","ContactFieldsZone","hasContactFieldsWidget","id","length","saveFields","fields","updatedAt","now","orgSlug","subdomain","host","createHostVersion","promoteForm","recountFormStats","router","enqueueSnackbar","queueLoading","data","status","idField","notFound","formNodes","decodeStoredNodes","nodes","versionDocs","versions","sort","a","b","createdAt","seconds","publishedVersionId","versionId","setName","lead","setLead","consentField","setConsentField","campaignIds","setCampaignIds","opening","setOpening","promoting","setPromoting","promoteRefusal","setPromoteRefusal","declaredFields","Array","isArray","effectiveLead","routing","effectiveConsent","String","consentFieldName","hasEmailField","capturesConsent","contactsHref","siteCampaigns","enabled","Boolean","storedCampaigns","readContainerIds","effectiveCampaigns","violations","checkFormContract","formNodeId","findFormNodeId","handleSave","dequeue","displayName","trim","formListFields","slug","formCampaignFields","variant","persist","formIds","error","console","allowDuplicate","handlePromote","targetVersionId","result","ok","message","handleOpen","$id","createResourceUid","kind","parentId","Object","keys","CANVAS_ROOT_ELEMENT_ID","componentId","rootId","push","PLUGIN_DOCUMENT_BESIGNER","documentSegment","docId","dirty","containerMembershipUnchanged","publishBlock","header","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","sx","alignItems","severity","href","cards","items","size","xs","lg","children","label","value","onChange","event","target","fullWidth","color","options","helperText","empty","ready","direction","disabled","onClick","control","checked","select","map","field","fieldName","component","pl","m","violation","code","nodeId","slot","loading","stats","leadRouting","onClose","align","version","hover","toDate","toLocaleString","justifyContent","title","undefined","startIcon","path","find"],"mappings":"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"}
|
|
@@ -15,6 +15,15 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { FormFieldDecl } from '@aglyn/aglyn';
|
|
18
|
+
import type { ConsoleHostScreensZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins';
|
|
19
|
+
/**
|
|
20
|
+
* The Forms page of a site, beside its Create Form action (AGL-3043): another
|
|
21
|
+
* way to start a form. A widget here is handed the site and its org — the
|
|
22
|
+
* `hostScreens` contract a site's pages, templates and components hand too —
|
|
23
|
+
* and opens its own way to a new form; it writes nothing through the page.
|
|
24
|
+
*/
|
|
25
|
+
export type HostFormsZoneProps = ConsoleHostScreensZoneProps;
|
|
26
|
+
export declare const HOST_FORMS_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<ConsoleHostScreensZoneProps>;
|
|
18
27
|
/**
|
|
19
28
|
* What the forms plugin hands a widget that reads ONE form's submissions.
|
|
20
29
|
*
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { definePluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
|
|
17
|
+
export const HOST_FORMS_ZONE = definePluginZone('hostForms');
|
|
17
18
|
export const FORM_SUBMISSIONS_ZONE = definePluginZone('formSubmissions');
|
|
18
19
|
export const FORM_CONTACT_FIELDS_ZONE = definePluginZone('formContactFields');
|
|
19
20
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-zones.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { FormFieldDecl } from '@aglyn/aglyn'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * What the forms plugin hands a widget that reads ONE form's submissions.\n *\n * The forms plugin owns the form and the page it is read on; it does not own\n * the reader. Reading submissions — the paged, ordered walk, the read marks,\n * the replies, the attribution — is what a plugin like the Inbox is for, and\n * this zone is where one draws it, scoped to this form, without the forms\n * plugin importing it.\n */\nexport interface FormSubmissionsZoneProps {\n hostId: string\n formId: string\n}\n\nexport const FORM_SUBMISSIONS_ZONE =\n definePluginZone<FormSubmissionsZoneProps>('formSubmissions')\n\n/**\n * Where each of a form's fields saves on the person, decided by whichever\n * plugin keeps people.\n *\n * A form declares its fields; what a contact's fields are is another plugin's\n * to know. This zone sits on the form's page beside routing and the consent\n * field and hands that plugin the published declaration. A widget here decides\n * what the declaration should become and calls `saveFields`; the page does the\n * write, because the form document is this plugin's.\n */\nexport interface FormContactFieldsZoneProps {\n hostId: string\n formId: string\n /** The PUBLISHED declaration, as stored on the form document. */\n fields: readonly FormFieldDecl[]\n /** True while the form document is still being read. */\n loading?: boolean\n /** Writes the declaration back onto the form document. */\n saveFields: (fields: FormFieldDecl[]) => Promise<void>\n}\n\nexport const FORM_CONTACT_FIELDS_ZONE =\n definePluginZone<FormContactFieldsZoneProps>('formContactFields')\n"],"names":["definePluginZone","FORM_SUBMISSIONS_ZONE","FORM_CONTACT_FIELDS_ZONE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-zones.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { FormFieldDecl } from '@aglyn/aglyn'\nimport type { ConsoleHostScreensZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * The Forms page of a site, beside its Create Form action (AGL-3043): another\n * way to start a form. A widget here is handed the site and its org — the\n * `hostScreens` contract a site's pages, templates and components hand too —\n * and opens its own way to a new form; it writes nothing through the page.\n */\nexport type HostFormsZoneProps = ConsoleHostScreensZoneProps\n\nexport const HOST_FORMS_ZONE = definePluginZone<HostFormsZoneProps>('hostForms')\n\n/**\n * What the forms plugin hands a widget that reads ONE form's submissions.\n *\n * The forms plugin owns the form and the page it is read on; it does not own\n * the reader. Reading submissions — the paged, ordered walk, the read marks,\n * the replies, the attribution — is what a plugin like the Inbox is for, and\n * this zone is where one draws it, scoped to this form, without the forms\n * plugin importing it.\n */\nexport interface FormSubmissionsZoneProps {\n hostId: string\n formId: string\n}\n\nexport const FORM_SUBMISSIONS_ZONE =\n definePluginZone<FormSubmissionsZoneProps>('formSubmissions')\n\n/**\n * Where each of a form's fields saves on the person, decided by whichever\n * plugin keeps people.\n *\n * A form declares its fields; what a contact's fields are is another plugin's\n * to know. This zone sits on the form's page beside routing and the consent\n * field and hands that plugin the published declaration. A widget here decides\n * what the declaration should become and calls `saveFields`; the page does the\n * write, because the form document is this plugin's.\n */\nexport interface FormContactFieldsZoneProps {\n hostId: string\n formId: string\n /** The PUBLISHED declaration, as stored on the form document. */\n fields: readonly FormFieldDecl[]\n /** True while the form document is still being read. */\n loading?: boolean\n /** Writes the declaration back onto the form document. */\n saveFields: (fields: FormFieldDecl[]) => Promise<void>\n}\n\nexport const FORM_CONTACT_FIELDS_ZONE =\n definePluginZone<FormContactFieldsZoneProps>('formContactFields')\n"],"names":["definePluginZone","HOST_FORMS_ZONE","FORM_SUBMISSIONS_ZONE","FORM_CONTACT_FIELDS_ZONE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SAASA,gBAAgB,QAAQ,2CAA0C;AAU3E,OAAO,MAAMC,kBAAkBD,iBAAqC,aAAY;AAgBhF,OAAO,MAAME,wBACXF,iBAA2C,mBAAkB;AAuB/D,OAAO,MAAMG,2BACXH,iBAA6C,qBAAoB"}
|