@murumets-ee/forms-ui 0.98.0 → 0.99.0
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/dist/pages.d.mts.map +1 -1
- package/dist/pages.mjs +1 -1
- package/dist/pages.mjs.map +1 -1
- package/dist/plugin.d.mts +16 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/package.json +8 -7
package/dist/pages.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages.d.mts","names":[],"sources":["../src/pages/form-canvas-page.tsx","../src/pages/forms-list-page.tsx"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"pages.d.mts","names":[],"sources":["../src/pages/form-canvas-page.tsx","../src/pages/forms-list-page.tsx"],"mappings":";;;;iBAuBsB,cAAA,CAAA;EACpB,MAAA;EACA;AAAA,GACC,SAAA;EAAY,MAAA;EAAgB,EAAA;AAAA,KAAgB,OAAA,CAAQ,SAAA;;;iBCQjC,aAAA,CAAA;EAAgB;AAAA,GAAU,SAAA;EAAY,MAAA;AAAA,KAAoB,OAAA,CAAQ,SAAA"}
|
package/dist/pages.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{getRegisteredAdminConfig as e}from"@murumets-ee/admin-ui/config-registry";import{hasEntityPermission as t,hasResourcePermission as n,renderEntityEditFallback as r}from"@murumets-ee/admin-ui/pages";import{
|
|
1
|
+
import{getRegisteredAdminConfig as e}from"@murumets-ee/admin-ui/config-registry";import{hasEntityPermission as t,hasResourcePermission as n,renderEntityEditFallback as r}from"@murumets-ee/admin-ui/pages";import{FORMS_SETTINGS_NAMESPACE as i,FORM_ENTITY as a,Form as o,formSpamCounts as s,formsSettings as c}from"@murumets-ee/forms";import{FormCanvasFactsProvider as l}from"@murumets-ee/forms/editor";import{notFound as u}from"next/navigation";import{getApp as d,requireRequestSite as f}from"@murumets-ee/core";import{challengeSecretFor as p}from"@murumets-ee/forms/server";import{SettingsClient as m}from"@murumets-ee/settings";import{jsx as h,jsxs as g}from"react/jsx-runtime";import{fetchEntityList as _}from"@murumets-ee/admin-ui/server";import{WorkflowStatusBadge as v}from"@murumets-ee/admin-ui/workflow";import{countEmbedUsages as y}from"@murumets-ee/blocks/server";import{NewFormDialog as b}from"@murumets-ee/forms-ui";import{Table as x,TableBody as S,TableCell as C,TableHead as w,TableHeader as T,TableRow as E}from"@murumets-ee/ui";import D from"next/link";import{getTranslations as O,setRequestLocale as k}from"next-intl/server";async function A(e){let t=new Map;if(e.length===0)return t;let n=await s.makeClient(d().db.readWrite).findMany({where:{formId:{in:[...e]}},limit:e.length});for(let e of n)t.set(e.formId,e.discarded);return t}async function j(){let e=d(),t=await f(i,e),n=new m(c,{app:e,site:t}),[r,a]=await Promise.all([n.get(`turnstileSiteKey`),n.get(`recaptchaSiteKey`)]),o=e=>typeof e==`string`&&e!==``;return{turnstile:o(r)&&p(`turnstile`,t,process.env)!==void 0,recaptcha:o(a)&&p(`recaptcha`,t,process.env)!==void 0}}const M=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function N({params:t,searchParams:n}){let{id:a,locale:s}=await t;M.test(a)||u();let c=e(),d=await r(o,c,{params:t,searchParams:n}),f=await c.withAdminContext(s,async()=>{let[e,t]=await Promise.all([A([a]).catch(()=>(console.warn(`forms: the blocked count for form ${a} could not be read`),null)),j()]);return{discarded:e===null?null:e.get(a)??0,ready:t,settingsHref:`${c.adminBasePath.replace(`:locale`,s)}/settings/${i}`}}).catch(()=>(console.warn(`forms: the spam facts for form ${a} could not be read`),null));return f===null?d:h(l,{facts:f,children:d})}async function P({params:r}){let i=e(),{locale:s}=await r;k(s);let c=await O(`Forms.list`),l=`${i.adminBasePath.replace(`:locale`,s)}/forms`;return i.withAdminContext(s,async()=>{await t(o,`view`,i)||u();let[{items:e,total:r},s]=await Promise.all([_(o,{sortField:`name`,sortDirection:`asc`,limit:200}),n(`form`,`create`,i)]),d=await y(a,e.map(e=>e.id)).catch(()=>null),f=await A(e.map(e=>String(e.id))).catch(()=>(console.warn(`forms: the blocked counts for the Forms list could not be read`),null));return g(`div`,{className:`space-y-4 p-6`,children:[g(`div`,{className:`flex items-start justify-between gap-4`,children:[g(`div`,{children:[h(`h1`,{className:`text-2xl font-bold`,children:c(`title`)}),h(`p`,{className:`text-sm text-muted-foreground`,children:c(`description`)})]}),s&&h(b,{apiBasePath:i.apiBasePath??`/api/admin`,formsBasePath:l})]}),e.length===0?h(`p`,{className:`rounded-md border border-dashed p-6 text-sm text-muted-foreground`,children:c(`empty`)}):g(x,{children:[h(T,{children:g(E,{children:[h(w,{children:c(`name`)}),h(w,{children:c(`usedOn`)}),h(w,{className:`text-right`,children:c(`submissions`)}),h(w,{className:`text-right`,children:c(`new`)}),h(w,{className:`text-right`,children:c(`blocked`)}),h(w,{children:c(`status`)})]})}),h(S,{children:e.map(e=>g(E,{children:[h(C,{className:`font-medium`,children:h(D,{href:`${l}/${e.id}`,className:`text-primary hover:underline`,children:e.name})}),h(C,{className:`text-muted-foreground`,"data-forms-used-on":e.id,children:d===null?c(`notYet`):c(`usedOnPages`,{count:d.get(e.id)??0})}),h(C,{className:`text-right text-muted-foreground`,children:c(`notYet`)}),h(C,{className:`text-right text-muted-foreground`,children:c(`notYet`)}),h(C,{className:`text-right tabular-nums`,children:f===null?c(`notYet`):f.get(String(e.id))??0}),h(C,{children:h(v,{status:e.status===`published`?`published`:`draft`})})]},e.id))})]}),r>e.length&&h(`p`,{className:`text-xs text-muted-foreground`,children:c(`truncated`,{count:e.length})})]})})}export{N as FormCanvasPage,P as FormsListPage};
|
|
2
2
|
//# sourceMappingURL=pages.mjs.map
|
package/dist/pages.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages.mjs","names":[],"sources":["../src/pages/form-canvas-page.tsx","../src/pages/forms-list-page.tsx"],"sourcesContent":["/**\n * The form canvas (W2) — a form open on its own canvas, in the `form`\n * environment.\n *\n * It owns `/admin/forms/:id` (and the singular `/admin/form/:id`), the URLs\n * the generic entity edit page would have answered, so a form has one editor and it is gated by the\n * job permission: the route's `forms:manage`, then — inside\n * `renderEntityEditFallback` — the entity's own `form:update`, which is also\n * what every write from the canvas is checked against. The editor itself is\n * the toolkit's: drafts, publish, locks, undo — nothing re-implemented.\n */\n\nimport { getRegisteredAdminConfig } from '@murumets-ee/admin-ui/config-registry'\nimport { renderEntityEditFallback } from '@murumets-ee/admin-ui/pages'\nimport type { PageProps } from '@murumets-ee/core'\nimport { Form } from '@murumets-ee/forms'\nimport { notFound } from 'next/navigation'\nimport type { ReactNode } from 'react'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nexport async function FormCanvasPage({\n params,\n searchParams,\n}: PageProps<{ locale: string; id: string }>): Promise<ReactNode> {\n const { id } = await params\n // `forms/new` lands here too (this route shadows the entity family); a form\n // is only ever created from the Forms list, so anything but an id is a 404.\n if (!UUID_RE.test(id)) notFound()\n return renderEntityEditFallback(Form, getRegisteredAdminConfig(), {\n params,\n searchParams,\n })\n}\n","/**\n * Forms (W1) — the selected site's forms, and the way into each one's canvas.\n *\n * Gated by `forms:manage` on its route (the plugin's `defaultRoutes`), which\n * is also what the sidebar link inherits. Lists THIS site's forms only: `form`\n * is site-bound, so the entity read is scoped by the request's site like\n * every other bound entity (C2).\n *\n * The columns are the approved wireframe's. \"Used on\" counts the PUBLISHED\n * pages that show the form (forms D002 — from `entity_refs`, which a page's\n * Form block feeds when the page is published); \"Submissions\", \"New\" and\n * \"Blocked\" fill in with the submission path (PR04/PR05). Until then each of\n * those shows a dash rather than a zero — a zero would claim a count that\n * nothing has taken yet.\n */\n\nimport { getRegisteredAdminConfig } from '@murumets-ee/admin-ui/config-registry'\nimport { hasEntityPermission, hasResourcePermission } from '@murumets-ee/admin-ui/pages'\nimport { fetchEntityList } from '@murumets-ee/admin-ui/server'\nimport { WorkflowStatusBadge } from '@murumets-ee/admin-ui/workflow'\nimport { countEmbedUsages } from '@murumets-ee/blocks/server'\nimport type { PageProps } from '@murumets-ee/core'\nimport { FORM_ENTITY, Form } from '@murumets-ee/forms'\nimport { NewFormDialog } from '@murumets-ee/forms-ui'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@murumets-ee/ui'\nimport Link from 'next/link'\nimport { notFound } from 'next/navigation'\nimport { getTranslations, setRequestLocale } from 'next-intl/server'\nimport type { ReactNode } from 'react'\n\n/** A site's forms are a handful; the bound keeps one page load one query. */\nconst MAX_FORMS = 200\n\nexport async function FormsListPage({ params }: PageProps<{ locale: string }>): Promise<ReactNode> {\n const config = getRegisteredAdminConfig()\n const { locale } = await params\n setRequestLocale(locale)\n const t = await getTranslations('Forms.list')\n const formsBasePath = `${config.adminBasePath.replace(':locale', locale)}/forms`\n\n return config.withAdminContext(locale, async () => {\n // The route's gate is the job (`forms:manage`); reading the rows is the\n // entity's own `form:view`. A role holding the first without the second\n // gets the same no-leak 404 as the gate, not a failed read.\n if (!(await hasEntityPermission(Form, 'view', config))) notFound()\n const [{ items, total }, canCreate] = await Promise.all([\n fetchEntityList(Form, { sortField: 'name', sortDirection: 'asc', limit: MAX_FORMS }),\n hasResourcePermission('form', 'create', config),\n ])\n // One grouped query for every listed form. A failure costs the column its\n // numbers (a dash), never the list.\n const usedOn = await countEmbedUsages(\n FORM_ENTITY,\n items.map((form) => form.id),\n ).catch(() => null)\n\n return (\n <div className=\"space-y-4 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-bold\">{t('title')}</h1>\n <p className=\"text-sm text-muted-foreground\">{t('description')}</p>\n </div>\n {canCreate && (\n <NewFormDialog\n apiBasePath={config.apiBasePath ?? '/api/admin'}\n formsBasePath={formsBasePath}\n />\n )}\n </div>\n\n {items.length === 0 ? (\n <p className=\"rounded-md border border-dashed p-6 text-sm text-muted-foreground\">\n {t('empty')}\n </p>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{t('name')}</TableHead>\n <TableHead>{t('usedOn')}</TableHead>\n <TableHead className=\"text-right\">{t('submissions')}</TableHead>\n <TableHead className=\"text-right\">{t('new')}</TableHead>\n <TableHead className=\"text-right\">{t('blocked')}</TableHead>\n <TableHead>{t('status')}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {items.map((form) => (\n <TableRow key={form.id}>\n <TableCell className=\"font-medium\">\n <Link\n href={`${formsBasePath}/${form.id}`}\n className=\"text-primary hover:underline\"\n >\n {form.name}\n </Link>\n </TableCell>\n <TableCell className=\"text-muted-foreground\" data-forms-used-on={form.id}>\n {usedOn === null\n ? t('notYet')\n : t('usedOnPages', { count: usedOn.get(form.id) ?? 0 })}\n </TableCell>\n <TableCell className=\"text-right text-muted-foreground\">{t('notYet')}</TableCell>\n <TableCell className=\"text-right text-muted-foreground\">{t('notYet')}</TableCell>\n <TableCell className=\"text-right text-muted-foreground\">{t('notYet')}</TableCell>\n <TableCell>\n <WorkflowStatusBadge\n status={form.status === 'published' ? 'published' : 'draft'}\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n {total > items.length && (\n <p className=\"text-xs text-muted-foreground\">{t('truncated', { count: items.length })}</p>\n )}\n </div>\n )\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pages.mjs","names":[],"sources":["../src/pages/form-facts.ts","../src/pages/form-canvas-page.tsx","../src/pages/forms-list-page.tsx"],"sourcesContent":["/**\n * What the Forms list (W1) and the form canvas (W2) show about the spam\n * protection (forms D004): how many submissions each form has dropped, and\n * whether the site can run each extra check.\n *\n * Read on the SERVER, inside the page's admin context, behind the page's own\n * gate (`forms:manage`). Counts only — nothing a visitor sent.\n */\n\nimport { getApp, requireRequestSite } from '@murumets-ee/core'\nimport { FORMS_SETTINGS_NAMESPACE, formSpamCounts, formsSettings } from '@murumets-ee/forms'\nimport { challengeSecretFor } from '@murumets-ee/forms/server'\nimport { SettingsClient } from '@murumets-ee/settings'\n\n/** Dropped-as-spam counts for `formIds` (at most a page's worth), by id; absent = 0. */\nexport async function discardedCounts(formIds: readonly string[]): Promise<Map<string, number>> {\n const counts = new Map<string, number>()\n if (formIds.length === 0) return counts\n const rows = await formSpamCounts.makeClient(getApp().db.readWrite).findMany({\n where: { formId: { in: [...formIds] } },\n limit: formIds.length,\n })\n for (const row of rows) counts.set(row.formId, row.discarded)\n return counts\n}\n\n/** Whether this site can run each extra check: a site key in settings AND a secret on the server. */\nexport async function challengeReadiness(): Promise<{ turnstile: boolean; recaptcha: boolean }> {\n const app = getApp()\n const site = await requireRequestSite(FORMS_SETTINGS_NAMESPACE, app)\n const settings = new SettingsClient(formsSettings, { app, site })\n const [turnstileKey, recaptchaKey] = await Promise.all([\n settings.get('turnstileSiteKey'),\n settings.get('recaptchaSiteKey'),\n ])\n const has = (value: unknown) => typeof value === 'string' && value !== ''\n return {\n turnstile:\n has(turnstileKey) && challengeSecretFor('turnstile', site, process.env) !== undefined,\n recaptcha:\n has(recaptchaKey) && challengeSecretFor('recaptcha', site, process.env) !== undefined,\n }\n}\n","/**\n * The form canvas (W2) — a form open on its own canvas, in the `form`\n * environment.\n *\n * It owns `/admin/forms/:id` (and the singular `/admin/form/:id`), the URLs\n * the generic entity edit page would have answered, so a form has one editor and it is gated by the\n * job permission: the route's `forms:manage`, then — inside\n * `renderEntityEditFallback` — the entity's own `form:update`, which is also\n * what every write from the canvas is checked against. The editor itself is\n * the toolkit's: drafts, publish, locks, undo — nothing re-implemented.\n */\n\nimport { getRegisteredAdminConfig } from '@murumets-ee/admin-ui/config-registry'\nimport { renderEntityEditFallback } from '@murumets-ee/admin-ui/pages'\nimport type { PageProps } from '@murumets-ee/core'\nimport { FORMS_SETTINGS_NAMESPACE, Form } from '@murumets-ee/forms'\nimport { FormCanvasFactsProvider } from '@murumets-ee/forms/editor'\nimport { notFound } from 'next/navigation'\nimport type { ReactNode } from 'react'\nimport { challengeReadiness, discardedCounts } from './form-facts.js'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nexport async function FormCanvasPage({\n params,\n searchParams,\n}: PageProps<{ locale: string; id: string }>): Promise<ReactNode> {\n const { id, locale } = await params\n // `forms/new` lands here too (this route shadows the entity family); a form\n // is only ever created from the Forms list, so anything but an id is a 404.\n if (!UUID_RE.test(id)) notFound()\n const config = getRegisteredAdminConfig()\n // The editor FIRST: it is what refuses another site's id or a role without\n // `form:update` (a no-leak 404), so nothing below runs for a form the\n // person may not open.\n const editor = await renderEntityEditFallback(Form, config, { params, searchParams })\n // What only the server knows, for the Form panel's Spam tab (D004): the\n // blocked count and which extra checks this site can run. Supplementary:\n // a failed read never costs the editor. Without the count the panel leaves\n // that line out; without the readiness it gets no facts at all, as on a\n // canvas that does not provide them.\n const facts = await config\n .withAdminContext(locale, async () => {\n const [counts, ready] = await Promise.all([\n discardedCounts([id]).catch(() => {\n // Never the error object: only the fact, and which form.\n console.warn(`forms: the blocked count for form ${id} could not be read`)\n return null\n }),\n challengeReadiness(),\n ])\n return {\n discarded: counts === null ? null : (counts.get(id) ?? 0),\n ready,\n settingsHref: `${config.adminBasePath.replace(':locale', locale)}/settings/${FORMS_SETTINGS_NAMESPACE}`,\n }\n })\n .catch(() => {\n console.warn(`forms: the spam facts for form ${id} could not be read`)\n return null\n })\n if (facts === null) return editor\n return <FormCanvasFactsProvider facts={facts}>{editor}</FormCanvasFactsProvider>\n}\n","/**\n * Forms (W1) — the selected site's forms, and the way into each one's canvas.\n *\n * Gated by `forms:manage` on its route (the plugin's `defaultRoutes`), which\n * is also what the sidebar link inherits. Lists THIS site's forms only: `form`\n * is site-bound, so the entity read is scoped by the request's site like\n * every other bound entity (C2).\n *\n * The columns are the approved wireframe's. \"Used on\" counts the PUBLISHED\n * pages that show the form (forms D002 — from `entity_refs`, which a page's\n * Form block feeds when the page is published); \"Blocked\" counts what the spam\n * checks dropped (PR03, D004); \"Submissions\" and \"New\" fill in with the\n * submissions screen (PR04). Until then those two show a dash rather than a\n * zero — a zero would claim a count that nothing has taken yet.\n */\n\nimport { getRegisteredAdminConfig } from '@murumets-ee/admin-ui/config-registry'\nimport { hasEntityPermission, hasResourcePermission } from '@murumets-ee/admin-ui/pages'\nimport { fetchEntityList } from '@murumets-ee/admin-ui/server'\nimport { WorkflowStatusBadge } from '@murumets-ee/admin-ui/workflow'\nimport { countEmbedUsages } from '@murumets-ee/blocks/server'\nimport type { PageProps } from '@murumets-ee/core'\nimport { FORM_ENTITY, Form } from '@murumets-ee/forms'\nimport { NewFormDialog } from '@murumets-ee/forms-ui'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@murumets-ee/ui'\nimport Link from 'next/link'\nimport { notFound } from 'next/navigation'\nimport { getTranslations, setRequestLocale } from 'next-intl/server'\nimport type { ReactNode } from 'react'\nimport { discardedCounts } from './form-facts.js'\n\n/** A site's forms are a handful; the bound keeps one page load one query. */\nconst MAX_FORMS = 200\n\nexport async function FormsListPage({ params }: PageProps<{ locale: string }>): Promise<ReactNode> {\n const config = getRegisteredAdminConfig()\n const { locale } = await params\n setRequestLocale(locale)\n const t = await getTranslations('Forms.list')\n const formsBasePath = `${config.adminBasePath.replace(':locale', locale)}/forms`\n\n return config.withAdminContext(locale, async () => {\n // The route's gate is the job (`forms:manage`); reading the rows is the\n // entity's own `form:view`. A role holding the first without the second\n // gets the same no-leak 404 as the gate, not a failed read.\n if (!(await hasEntityPermission(Form, 'view', config))) notFound()\n const [{ items, total }, canCreate] = await Promise.all([\n fetchEntityList(Form, { sortField: 'name', sortDirection: 'asc', limit: MAX_FORMS }),\n hasResourcePermission('form', 'create', config),\n ])\n // One grouped query for every listed form. A failure costs the column its\n // numbers (a dash), never the list.\n const usedOn = await countEmbedUsages(\n FORM_ENTITY,\n items.map((form) => form.id),\n ).catch(() => null)\n // Dropped-as-spam per form (D004) — one bounded read for the listed forms.\n // A failure costs the column its numbers (a dash), never the list.\n const blocked = await discardedCounts(items.map((form) => String(form.id))).catch(() => {\n console.warn('forms: the blocked counts for the Forms list could not be read')\n return null\n })\n\n return (\n <div className=\"space-y-4 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-bold\">{t('title')}</h1>\n <p className=\"text-sm text-muted-foreground\">{t('description')}</p>\n </div>\n {canCreate && (\n <NewFormDialog\n apiBasePath={config.apiBasePath ?? '/api/admin'}\n formsBasePath={formsBasePath}\n />\n )}\n </div>\n\n {items.length === 0 ? (\n <p className=\"rounded-md border border-dashed p-6 text-sm text-muted-foreground\">\n {t('empty')}\n </p>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{t('name')}</TableHead>\n <TableHead>{t('usedOn')}</TableHead>\n <TableHead className=\"text-right\">{t('submissions')}</TableHead>\n <TableHead className=\"text-right\">{t('new')}</TableHead>\n <TableHead className=\"text-right\">{t('blocked')}</TableHead>\n <TableHead>{t('status')}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {items.map((form) => (\n <TableRow key={form.id}>\n <TableCell className=\"font-medium\">\n <Link\n href={`${formsBasePath}/${form.id}`}\n className=\"text-primary hover:underline\"\n >\n {form.name}\n </Link>\n </TableCell>\n <TableCell className=\"text-muted-foreground\" data-forms-used-on={form.id}>\n {usedOn === null\n ? t('notYet')\n : t('usedOnPages', { count: usedOn.get(form.id) ?? 0 })}\n </TableCell>\n <TableCell className=\"text-right text-muted-foreground\">{t('notYet')}</TableCell>\n <TableCell className=\"text-right text-muted-foreground\">{t('notYet')}</TableCell>\n <TableCell className=\"text-right tabular-nums\">\n {blocked === null ? t('notYet') : (blocked.get(String(form.id)) ?? 0)}\n </TableCell>\n <TableCell>\n <WorkflowStatusBadge\n status={form.status === 'published' ? 'published' : 'draft'}\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n {total > items.length && (\n <p className=\"text-xs text-muted-foreground\">{t('truncated', { count: items.length })}</p>\n )}\n </div>\n )\n })\n}\n"],"mappings":"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"}
|
package/dist/plugin.d.mts
CHANGED
|
@@ -176,6 +176,22 @@ declare function formsPlugin(): Pick<import("@murumets-ee/core").Plugin, "server
|
|
|
176
176
|
readonly name: "@murumets-ee/forms";
|
|
177
177
|
readonly shared?: {
|
|
178
178
|
readonly features: readonly DefineFeatureSpec[];
|
|
179
|
+
readonly settings: readonly [import("@murumets-ee/settings").SettingsDefinition<{
|
|
180
|
+
readonly turnstileSiteKey: import("@murumets-ee/settings").TextSettingConfig & {
|
|
181
|
+
readonly default: "";
|
|
182
|
+
readonly maxLength: 100;
|
|
183
|
+
readonly pattern: RegExp;
|
|
184
|
+
readonly label: "Cloudflare Turnstile site key";
|
|
185
|
+
readonly description: "From the Turnstile widget in your Cloudflare dashboard. The secret key is set on the server as FORMS_TURNSTILE_SECRET, never here.";
|
|
186
|
+
};
|
|
187
|
+
readonly recaptchaSiteKey: import("@murumets-ee/settings").TextSettingConfig & {
|
|
188
|
+
readonly default: "";
|
|
189
|
+
readonly maxLength: 100;
|
|
190
|
+
readonly pattern: RegExp;
|
|
191
|
+
readonly label: "Google reCAPTCHA v3 site key";
|
|
192
|
+
readonly description: "From the reCAPTCHA admin console (v3). The secret key is set on the server as FORMS_RECAPTCHA_SECRET, never here. reCAPTCHA sends visitor data to Google \u2014 mention it in your privacy notice.";
|
|
193
|
+
};
|
|
194
|
+
}, "forms">];
|
|
179
195
|
readonly messages: typeof getFormsMessages;
|
|
180
196
|
};
|
|
181
197
|
};
|
package/dist/plugin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","names":["PermissionString","AdminRouteMethod","SegmentPath","P","DefineAdminRouteSpec","TApp","AdminRouteHandler","AdminRouteMcpSpec","prefix","path","method","permission","defaultRoles","description","descriptionKey","handler","matchAnyPath","tokens","mcp","confirm","JsonValue","key","McpInputSchema","type","properties","required","additionalProperties","inputSchema","ENTRY_GATED","AdminRouteEntry","resource","action","guardedHandler","acceptsTokens","requiresConfirmation","PermissionCatalogEntry","source","registerPermission","entry","getPermissionCatalog","Map","_resetPermissionCatalog","clearFeaturePermissions","safeAudit","Parameters","NonNullable","ctx","safeAuditMany","Promise","entries","PermissionDenialContext","Record","extraMetadata","emitPermissionDenied","spec","permissionDeniedResponse","Response","role","defineAdminRoute","DefineFeatureSpec","actions","defineFeature","combineAdminRoutes","AdminRoute","AuthDoor","TOKEN_DERIVED_DOORS","ReadonlySet","isTokenDerivedDoor","door","CredentialNarrowing","Readonly","CallerCredential","id","narrowing","rateLimitPerMinute","CREDENTIAL_RATE_BUDGET_MAX","parseCredentialRateBudget","raw","AdminApiCaller","U","ConfirmationPolicy","name","email","user","credential","confirmation","narrowCheck","base","GATED","AuthUser","AuditLogFn","entityType","entityId","userId","userName","changes","metadata","AuditLogManyFn","PermissionChecker","Request","req","segments","locale","defaultLocale","audit","auditMany","checkPermission","app","TokenPolicyEntry","handlers","GET","POST","PATCH","DELETE","tokenPolicy","matchTokenPolicyEntry","Pick","route","routeRequiresConfirmation","routeAcceptsTokens","isGatedRoute","value","AGENT_APPROVALS_RESOURCE","APPROVAL_STATUSES","ApprovalStatus","isApprovalStatus","DEFAULT_APPROVAL_TTL_MS","MAX_PARKED_BODY_BYTES","MAX_PENDING_PER_PRINCIPAL","APPROVAL_ID_PATTERN","RegExp","ApprovalPreview","kind","entity","ids","snapshot","ParkedOperation","Date","principalId","credentialId","query","body","preview","status","requestedAt","expiresAt","decidedBy","decidedAt","resultStatus","result","ParkInput","ParkOutcome","ok","reason","AgentApprovalQueue","park","input","now","get","list","limit","offset","options","items","total","countPending","claim","reject","finish","outcome","resumeCaller","op","sweep","staleExecutingMs","expired","interrupted","BuiltInEntityAction","suffix","behavior","requires","needsContentClient","BUILT_IN_ENTITY_ACTIONS","ADMIN_SITE_HEADER","ADMIN_SITE_QUERY_PARAM","ADMIN_SITE_SEGMENT_PREFIX","isAdminSiteHandleShape","parseAdminSiteSegment","segment","isAdminSiteSegment","adminSiteSegment","site","siteFromAdminPath","pathname","withAdminSite","href","stripAdminSite"],"sources":["../../admin-route/dist/index.d.mts","../src/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;UAqvBU2D,iBAAAA;;;;;;EAMR7B,QAAAA;;;;;;;;EAQA8B,OAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0CAhD,YAAAA;;;;;;;EAOAC,WAAAA;AAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":["PermissionString","AdminRouteMethod","SegmentPath","P","DefineAdminRouteSpec","TApp","AdminRouteHandler","AdminRouteMcpSpec","prefix","path","method","permission","defaultRoles","description","descriptionKey","handler","matchAnyPath","tokens","mcp","confirm","JsonValue","key","McpInputSchema","type","properties","required","additionalProperties","inputSchema","ENTRY_GATED","AdminRouteEntry","resource","action","guardedHandler","acceptsTokens","requiresConfirmation","PermissionCatalogEntry","source","registerPermission","entry","getPermissionCatalog","Map","_resetPermissionCatalog","clearFeaturePermissions","safeAudit","Parameters","NonNullable","ctx","safeAuditMany","Promise","entries","PermissionDenialContext","Record","extraMetadata","emitPermissionDenied","spec","permissionDeniedResponse","Response","role","defineAdminRoute","DefineFeatureSpec","actions","defineFeature","combineAdminRoutes","AdminRoute","AuthDoor","TOKEN_DERIVED_DOORS","ReadonlySet","isTokenDerivedDoor","door","CredentialNarrowing","Readonly","CallerCredential","id","narrowing","rateLimitPerMinute","CREDENTIAL_RATE_BUDGET_MAX","parseCredentialRateBudget","raw","AdminApiCaller","U","ConfirmationPolicy","name","email","user","credential","confirmation","narrowCheck","base","GATED","AuthUser","AuditLogFn","entityType","entityId","userId","userName","changes","metadata","AuditLogManyFn","PermissionChecker","Request","req","segments","locale","defaultLocale","audit","auditMany","checkPermission","app","TokenPolicyEntry","handlers","GET","POST","PATCH","DELETE","tokenPolicy","matchTokenPolicyEntry","Pick","route","routeRequiresConfirmation","routeAcceptsTokens","isGatedRoute","value","AGENT_APPROVALS_RESOURCE","APPROVAL_STATUSES","ApprovalStatus","isApprovalStatus","DEFAULT_APPROVAL_TTL_MS","MAX_PARKED_BODY_BYTES","MAX_PENDING_PER_PRINCIPAL","APPROVAL_ID_PATTERN","RegExp","ApprovalPreview","kind","entity","ids","snapshot","ParkedOperation","Date","principalId","credentialId","query","body","preview","status","requestedAt","expiresAt","decidedBy","decidedAt","resultStatus","result","ParkInput","ParkOutcome","ok","reason","AgentApprovalQueue","park","input","now","get","list","limit","offset","options","items","total","countPending","claim","reject","finish","outcome","resumeCaller","op","sweep","staleExecutingMs","expired","interrupted","CONFIRMATION_REQUIRED_STATUS","CONFIRMATION_REQUIRED_CODE","BuiltInEntityAction","suffix","behavior","requires","needsContentClient","BUILT_IN_ENTITY_ACTIONS","ADMIN_SITE_HEADER","ADMIN_SITE_QUERY_PARAM","ADMIN_SITE_SEGMENT_PREFIX","isAdminSiteHandleShape","parseAdminSiteSegment","segment","isAdminSiteSegment","adminSiteSegment","site","siteFromAdminPath","pathname","withAdminSite","href","stripAdminSite"],"sources":["../../admin-route/dist/index.d.mts","../src/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;UAqvBU2D,iBAAAA;;;;;;EAMR7B,QAAAA;;;;;;;;EAQA8B,OAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0CAhD,YAAAA;;;;;;;EAOAC,WAAAA;AAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBC5vBc,WAAA,CAAA,GAAW,IAAA,6BAAA,MAAA;EAAA;;gCAAA,iBAAA;IAAA"}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{definePlugin as e}from"@murumets-ee/core";import{FORMS_MANAGE as t,Form as n,
|
|
1
|
+
import{definePlugin as e}from"@murumets-ee/core";import{FORMS_MANAGE as t,Form as n,FormSubmission as r,formSpamCounts as i,formsFeatures as a,formsRoutes as o,formsSettings as s}from"@murumets-ee/forms";import{getFormsMessages as c}from"@murumets-ee/forms/i18n";import{FormEmbedBlock as l,formEnvironment as u,formPageEnvironmentExtension as d,formSchemaBlocks as f}from"@murumets-ee/forms/schema";const p=async e=>(await import(`@murumets-ee/forms-ui/pages`)).FormsListPage(e),m=async e=>(await import(`@murumets-ee/forms-ui/pages`)).FormCanvasPage(e);function h(){return e({name:`@murumets-ee/forms`,requires:[`@murumets-ee/content`,`@murumets-ee/settings`],server:{entities:[n,r],tables:{forms_spam_counts:i.table},routes:[...o()]},contributes:{"blocks.blocks":[...f,l],"blocks.environmentBases":[u],"blocks.environments":d},shared:{features:a,settings:[s],messages:c},adminUi:{pages:{FormsListPage:p,FormCanvasPage:m},sidebar:[{id:`forms`,group:`Content`,label:`Forms`,href:`/admin/forms`,iconName:`clipboard-list`,requires:t}],defaultRoutes:[{path:`forms`,factory:`FormsListPage`,page:{requires:t,defaultRoles:[`admin`,`editor`],description:`List this site’s forms and create new ones.`,descriptionKey:`Forms.permissions.list`,tokens:`accept`}},{path:`forms/[id]`,factory:`FormCanvasPage`,page:{requires:t,defaultRoles:[`admin`,`editor`],description:`Edit a form on its canvas.`,descriptionKey:`Forms.permissions.canvas`,tokens:`accept`}},{path:`form`,factory:`FormsListPage`},{path:`form/[id]`,factory:`FormCanvasPage`}]}})}export{h as formsPlugin};
|
|
2
2
|
//# sourceMappingURL=plugin.mjs.map
|
package/dist/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * `formsPlugin()` — everything `@murumets-ee/forms` contributes, declared as\n * data on one Plugin object (CLAUDE.md \"all plugin contributions are\n * declarative\"). Drop it into `plugins:` and the framework wires:\n *\n * - the `form` entity (site-bound canvas document);\n * - the block vocabulary to the SERVER — `blocks.blocks` (so the tree walk\n * that judges every write knows `ancestor` / `uniqueKey`), the `form`\n * environment base (so the canvas resolves its palette), and the Form block\n * in the site's `page` environment (D002 — a saved form on any page);\n * - the admin route behind \"Detach a copy\";\n * - the permissions — `forms:manage` and the `form` entity grants — to the\n * Permission Matrix, seeded for `editor`;\n * - the catalogue (`shared.messages`);\n * - the Forms list and the form canvas as pages, and the sidebar link.\n *\n * The browser half — renders and the panel's field editors — is\n * `<FormBlocksRegistration />` from `@murumets-ee/forms-ui`, mounted by the\n * admin layout (the standing client-registry exception in CLAUDE.md).\n *\n * jiti-safe: this module is reached from `lumi.config.ts`, so the pages load\n * lazily and nothing here imports JSX or the blocks root.\n */\n\n// The `blocks.*` contribution slots are declared by module augmentation in\n// `@murumets-ee/blocks`; the type-only import is what brings them into scope.\nimport type {} from '@murumets-ee/blocks'\nimport { definePlugin, type PageFactory } from '@murumets-ee/core'\nimport {
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * `formsPlugin()` — everything `@murumets-ee/forms` contributes, declared as\n * data on one Plugin object (CLAUDE.md \"all plugin contributions are\n * declarative\"). Drop it into `plugins:` and the framework wires:\n *\n * - the `form` entity (site-bound canvas document), the `form_submission`\n * entity and the per-form spam count table;\n * - the per-site `forms` settings namespace (CAPTCHA site keys);\n * - the block vocabulary to the SERVER — `blocks.blocks` (so the tree walk\n * that judges every write knows `ancestor` / `uniqueKey`), the `form`\n * environment base (so the canvas resolves its palette), and the Form block\n * in the site's `page` environment (D002 — a saved form on any page);\n * - the admin route behind \"Detach a copy\";\n * - the permissions — `forms:manage` and the `form` entity grants — to the\n * Permission Matrix, seeded for `editor`;\n * - the catalogue (`shared.messages`);\n * - the Forms list and the form canvas as pages, and the sidebar link.\n *\n * The browser half — renders and the panel's field editors — is\n * `<FormBlocksRegistration />` from `@murumets-ee/forms-ui`, mounted by the\n * admin layout (the standing client-registry exception in CLAUDE.md).\n *\n * jiti-safe: this module is reached from `lumi.config.ts`, so the pages load\n * lazily and nothing here imports JSX or the blocks root.\n */\n\n// The `blocks.*` contribution slots are declared by module augmentation in\n// `@murumets-ee/blocks`; the type-only import is what brings them into scope.\nimport type {} from '@murumets-ee/blocks'\nimport { definePlugin, type PageFactory } from '@murumets-ee/core'\nimport {\n FORMS_MANAGE,\n Form,\n FormSubmission,\n formSpamCounts,\n formsFeatures,\n formsRoutes,\n formsSettings,\n} from '@murumets-ee/forms'\nimport { getFormsMessages } from '@murumets-ee/forms/i18n'\nimport {\n FormEmbedBlock,\n formEnvironment,\n formPageEnvironmentExtension,\n formSchemaBlocks,\n} from '@murumets-ee/forms/schema'\n\nconst FormsListPage: PageFactory<{ locale: string }> = async (props) => {\n const mod = await import('@murumets-ee/forms-ui/pages')\n return mod.FormsListPage(props)\n}\nconst FormCanvasPage: PageFactory<{ locale: string; id: string }> = async (props) => {\n const mod = await import('@murumets-ee/forms-ui/pages')\n return mod.FormCanvasPage(props)\n}\n\nexport function formsPlugin() {\n return definePlugin({\n name: '@murumets-ee/forms',\n // `publishable()` keeps its drafts in tables the content plugin owns; the\n // per-site Forms settings (CAPTCHA site keys) live in the settings plugin.\n requires: ['@murumets-ee/content', '@murumets-ee/settings'],\n server: {\n // `form_submission` is written only by the public submission path\n // (`@murumets-ee/forms/server`, mounted on the content API) and read by\n // the submissions screen (PR04).\n entities: [Form, FormSubmission],\n // The per-form \"dropped as spam\" counts (D004) — a number per form.\n tables: { forms_spam_counts: formSpamCounts.table },\n // `POST /api/admin/form-copy` — the Form block's \"Detach a copy\".\n routes: [...formsRoutes()],\n },\n contributes: {\n // The form document's blocks, and the page's Form block (D002).\n 'blocks.blocks': [...formSchemaBlocks, FormEmbedBlock],\n 'blocks.environmentBases': [formEnvironment],\n // The site's pages may hold a Form block, and the page environment\n // holds the form's blocks too, because a page DRAWS an embedded form\n // with the site theme (see `formPageEnvironmentExtension`).\n 'blocks.environments': formPageEnvironmentExtension,\n },\n shared: {\n features: formsFeatures,\n settings: [formsSettings],\n messages: getFormsMessages,\n },\n adminUi: {\n pages: {\n FormsListPage,\n FormCanvasPage,\n },\n sidebar: [\n {\n id: 'forms',\n group: 'Content',\n label: 'Forms',\n href: '/admin/forms',\n iconName: 'clipboard-list',\n requires: FORMS_MANAGE,\n },\n ],\n defaultRoutes: [\n {\n // Owns the `form` entity's list URL — the generic table never shows.\n path: 'forms',\n factory: 'FormsListPage',\n page: {\n requires: FORMS_MANAGE,\n defaultRoles: ['admin', 'editor'],\n description: 'List this site’s forms and create new ones.',\n descriptionKey: 'Forms.permissions.list',\n // An agent's browser session may open it (agent-access D020) —\n // the same reach its key already has to `form` through the API,\n // still behind its role's `forms:manage`.\n tokens: 'accept',\n },\n },\n {\n // Owns the `form` entity's edit URL — the canvas, gated by the job.\n path: 'forms/[id]',\n factory: 'FormCanvasPage',\n page: {\n requires: FORMS_MANAGE,\n defaultRoles: ['admin', 'editor'],\n description: 'Edit a form on its canvas.',\n descriptionKey: 'Forms.permissions.canvas',\n tokens: 'accept',\n },\n },\n // The SINGULAR slugs resolve to the same entity (`/admin/form/<id>`),\n // so they are owned too — otherwise the generic editor would answer\n // them behind `form:*` alone, and a form would have two addresses.\n // No `page` spec: the gate wraps the FACTORY once (above), so every\n // path that renders it is gated the same way.\n { path: 'form', factory: 'FormsListPage' },\n { path: 'form/[id]', factory: 'FormCanvasPage' },\n ],\n },\n })\n}\n"],"mappings":"+YA+CA,MAAM,EAAiD,KAAO,KAErD,MADW,OAAO,+BAAA,CACd,cAAc,CAAK,EAE1B,EAA8D,KAAO,KAElE,MADW,OAAO,+BAAA,CACd,eAAe,CAAK,EAGjC,SAAgB,GAAc,CAC5B,OAAO,EAAa,CAClB,KAAM,qBAGN,SAAU,CAAC,uBAAwB,uBAAuB,EAC1D,OAAQ,CAIN,SAAU,CAAC,EAAM,CAAc,EAE/B,OAAQ,CAAE,kBAAmB,EAAe,KAAM,EAElD,OAAQ,CAAC,GAAG,EAAY,CAAC,CAC3B,EACA,YAAa,CAEX,gBAAiB,CAAC,GAAG,EAAkB,CAAc,EACrD,0BAA2B,CAAC,CAAe,EAI3C,sBAAuB,CACzB,EACA,OAAQ,CACN,SAAU,EACV,SAAU,CAAC,CAAa,EACxB,SAAU,CACZ,EACA,QAAS,CACP,MAAO,CACL,gBACA,gBACF,EACA,QAAS,CACP,CACE,GAAI,QACJ,MAAO,UACP,MAAO,QACP,KAAM,eACN,SAAU,iBACV,SAAU,CACZ,CACF,EACA,cAAe,CACb,CAEE,KAAM,QACN,QAAS,gBACT,KAAM,CACJ,SAAU,EACV,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,8CACb,eAAgB,yBAIhB,OAAQ,QACV,CACF,EACA,CAEE,KAAM,aACN,QAAS,iBACT,KAAM,CACJ,SAAU,EACV,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,6BACb,eAAgB,2BAChB,OAAQ,QACV,CACF,EAMA,CAAE,KAAM,OAAQ,QAAS,eAAgB,EACzC,CAAE,KAAM,YAAa,QAAS,gBAAiB,CACjD,CACF,CACF,CAAC,CACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murumets-ee/forms-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Admin half of @murumets-ee/forms: the form canvas registration, the Forms list, the New form dialog and the plugin.",
|
|
@@ -22,12 +22,13 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@murumets-ee/admin-ui": "0.
|
|
26
|
-
"@murumets-ee/
|
|
27
|
-
"@murumets-ee/
|
|
28
|
-
"@murumets-ee/
|
|
29
|
-
"@murumets-ee/
|
|
30
|
-
"@murumets-ee/
|
|
25
|
+
"@murumets-ee/admin-ui": "0.99.0",
|
|
26
|
+
"@murumets-ee/content": "0.99.0",
|
|
27
|
+
"@murumets-ee/blocks": "0.99.0",
|
|
28
|
+
"@murumets-ee/core": "0.99.0",
|
|
29
|
+
"@murumets-ee/forms": "0.99.0",
|
|
30
|
+
"@murumets-ee/settings": "0.99.0",
|
|
31
|
+
"@murumets-ee/ui": "0.99.0"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"next": ">=16.0.0 <17",
|