@murumets-ee/forms-ui 0.0.0-stage → 0.97.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/LICENSE +94 -0
- package/dist/index.d.mts +25 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/dist/pages.d.mts +21 -0
- package/dist/pages.d.mts.map +1 -0
- package/dist/pages.mjs +2 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/plugin.d.mts +184 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/dist/plugin.mjs.map +1 -0
- package/package.json +70 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Elastic License 2.0 (ELv2)
|
|
2
|
+
|
|
3
|
+
URL: https://www.elastic.co/licensing/elastic-license
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
8
|
+
|
|
9
|
+
## Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
12
|
+
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
13
|
+
available, and prepare derivative works of the software, in each case subject
|
|
14
|
+
to the limitations and conditions below.
|
|
15
|
+
|
|
16
|
+
## Limitations
|
|
17
|
+
|
|
18
|
+
You may not provide the software to third parties as a hosted or managed
|
|
19
|
+
service, where the service provides users with access to any substantial set
|
|
20
|
+
of the features or functionality of the software.
|
|
21
|
+
|
|
22
|
+
You may not move, change, disable, or circumvent the license key functionality
|
|
23
|
+
in the software, and you may not remove or obscure any functionality in the
|
|
24
|
+
software that is protected by the license key.
|
|
25
|
+
|
|
26
|
+
You may not alter, remove, or obscure any licensing, copyright, or other
|
|
27
|
+
notices of the licensor in the software. Any use of the licensor's trademarks
|
|
28
|
+
is subject to applicable law.
|
|
29
|
+
|
|
30
|
+
## Patents
|
|
31
|
+
|
|
32
|
+
The licensor grants you a license, under any patent claims the licensor can
|
|
33
|
+
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
34
|
+
sale, import and have imported the software, in each case subject to the
|
|
35
|
+
limitations and conditions in this license. This license does not cover any
|
|
36
|
+
patent claims that you cause to be infringed by modifications or additions to
|
|
37
|
+
the software. If you or your company make any written claim that the software
|
|
38
|
+
infringes or contributes to infringement of any patent, your patent license
|
|
39
|
+
for the software granted under these terms ends immediately. If your company
|
|
40
|
+
makes such a claim, your patent license ends immediately for work on behalf
|
|
41
|
+
of your company.
|
|
42
|
+
|
|
43
|
+
## Notices
|
|
44
|
+
|
|
45
|
+
You must ensure that anyone who gets a copy of any part of the software from
|
|
46
|
+
you also gets a copy of these terms.
|
|
47
|
+
|
|
48
|
+
If you modify the software, you must include in any modified copies of the
|
|
49
|
+
software prominent notices stating that you have modified the software.
|
|
50
|
+
|
|
51
|
+
## No Other Rights
|
|
52
|
+
|
|
53
|
+
These terms do not imply any licenses other than those expressly granted in
|
|
54
|
+
these terms.
|
|
55
|
+
|
|
56
|
+
## Termination
|
|
57
|
+
|
|
58
|
+
If you use the software in violation of these terms, such use is not licensed,
|
|
59
|
+
and your licenses will automatically terminate. If the licensor provides you
|
|
60
|
+
with a notice of your violation, and you cease all violation of this license
|
|
61
|
+
no later than 30 days after you receive that notice, your licenses will be
|
|
62
|
+
reinstated retroactively. However, if you violate these terms after such
|
|
63
|
+
reinstatement, any additional violation of these terms will cause your
|
|
64
|
+
licenses to terminate automatically and permanently.
|
|
65
|
+
|
|
66
|
+
## No Liability
|
|
67
|
+
|
|
68
|
+
As far as the law allows, the software comes as is, without any warranty or
|
|
69
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
70
|
+
out of these terms or the use or nature of the software, under any kind of
|
|
71
|
+
legal claim.
|
|
72
|
+
|
|
73
|
+
## Definitions
|
|
74
|
+
|
|
75
|
+
The **licensor** is the entity offering these terms, and the **software** is
|
|
76
|
+
the software the licensor makes available under these terms, including any
|
|
77
|
+
portion of it.
|
|
78
|
+
|
|
79
|
+
**you** refers to the individual or entity agreeing to these terms.
|
|
80
|
+
|
|
81
|
+
**your company** is any legal entity, sole proprietorship, or other kind of
|
|
82
|
+
organization that you work for, plus all organizations that have control over,
|
|
83
|
+
are under the control of, or are under common control with that organization.
|
|
84
|
+
**control** means ownership of substantially all the assets of an entity, or
|
|
85
|
+
the power to direct the management and policies of an entity (for example, by
|
|
86
|
+
voting right, contract, or otherwise). Control can be direct or indirect.
|
|
87
|
+
|
|
88
|
+
**your licenses** are all the licenses granted to you for the software under
|
|
89
|
+
these terms.
|
|
90
|
+
|
|
91
|
+
**use** means anything you do with the software requiring one of your
|
|
92
|
+
licenses.
|
|
93
|
+
|
|
94
|
+
**trademark** means trademarks, service marks, and similar rights.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
import { ReactElement } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/new-form-dialog.d.ts
|
|
5
|
+
interface NewFormDialogProps {
|
|
6
|
+
readonly apiBasePath: string;
|
|
7
|
+
/** Where the form canvas lives — `<admin base>/forms`. */
|
|
8
|
+
readonly formsBasePath: string;
|
|
9
|
+
}
|
|
10
|
+
declare function NewFormDialog({
|
|
11
|
+
apiBasePath,
|
|
12
|
+
formsBasePath
|
|
13
|
+
}: NewFormDialogProps): ReactElement;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/register.d.ts
|
|
16
|
+
declare function registerFormBlocks(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Render it, do not import it for effect: a component in the layout is what
|
|
19
|
+
* guarantees the registration runs before any canvas resolves its vocabulary
|
|
20
|
+
* (`resolveBlockVocabulary` runs during render). Same shape as menus-ui's.
|
|
21
|
+
*/
|
|
22
|
+
declare function FormBlocksRegistration(): null;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { FormBlocksRegistration, NewFormDialog, type NewFormDialogProps, registerFormBlocks };
|
|
25
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/new-form-dialog.tsx","../src/register.tsx"],"mappings":";;;;UA8BiB,kBAAA;EAAA,SACN,WAAA;EADM;EAAA,SAGN,aAAa;AAAA;AAAA,iBAGR,aAAA,CAAA;EAAgB,WAAA;EAAa;AAAA,GAAiB,kBAAA,GAAqB,YAAA;;;iBCLnE,kBAAA,CAAA;;;ADDhB;;;iBCkCgB,sBAAA,CAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{FORMS_SOURCE as e,FORM_ENVIRONMENT as t,FORM_NAME_MAX as n,PAGE_ENVIRONMENT as r,formEnvironmentBlocks as i,formEnvironmentTraits as a,formSiteBlocks as o}from"@murumets-ee/forms/schema";import{Button as s,Dialog as c,DialogContent as l,DialogHeader as u,DialogTitle as d,Input as f,Label as p}from"@murumets-ee/ui";import{useRouter as m}from"next/navigation";import{useTranslations as h}from"next-intl";import{useState as g}from"react";import{Fragment as _,jsx as v,jsxs as y}from"react/jsx-runtime";import{registerBlockEnvironment as b,registerBlockEnvironmentBase as x,registerBlockTheme as S,widenFieldType as C}from"@murumets-ee/blocks/core";import{formFieldKeyType as w,formPickerType as T,formSettingsTabsType as E}from"@murumets-ee/forms/editor";import{FORMS_THEME as D,formsTheme as O}from"@murumets-ee/forms/render";function k({apiBasePath:e,formsBasePath:t}){let r=h(`Forms.newDialog`),i=m(),[a,o]=g(!1),[b,x]=g(``),[S,C]=g(!1),[w,T]=g(null),E=b.trim(),D=E!==``&&E.length<=n&&!S;return y(_,{children:[v(s,{type:`button`,size:`sm`,onClick:()=>o(!0),children:r(`title`)}),v(c,{open:a,onOpenChange:o,children:y(l,{className:`max-w-md`,children:[v(u,{children:v(d,{children:r(`title`)})}),y(`form`,{onSubmit:async n=>{if(n.preventDefault(),D){C(!0),T(null);try{let n=await fetch(`${e}/form`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:E})});if(!n.ok)throw Error(r(`failed`,{status:n.status}));let a=await n.json();if(typeof a.id!=`string`)throw Error(r(`noId`));o(!1),C(!1),i.push(`${t}/${a.id}`)}catch(e){T(e instanceof Error?e.message:r(`failed`,{status:0})),C(!1)}}},className:`flex flex-col gap-3`,children:[y(`div`,{className:`flex flex-col gap-1.5`,children:[v(p,{htmlFor:`new-form-name`,children:r(`name`)}),v(f,{id:`new-form-name`,value:b,maxLength:n,onChange:e=>x(e.target.value),placeholder:r(`namePlaceholder`),"aria-describedby":`new-form-name-help`}),v(`p`,{id:`new-form-name-help`,className:`text-xs text-muted-foreground`,children:r(`help`)})]}),w!==null&&v(`p`,{role:`alert`,className:`text-sm text-destructive`,children:w}),y(`div`,{className:`flex justify-end gap-2 pt-1`,children:[v(s,{type:`button`,variant:`outline`,onClick:()=>o(!1),children:r(`cancel`)}),v(s,{type:`submit`,disabled:!D,loading:S,children:r(`create`)})]})]})]})})]})}function A(){S(O),x({name:t,blocks:i,traits:a,fieldTypes:[C(w),C(E)],defaultTheme:D}),b({source:e,name:r,blocks:o,fieldTypes:[C(T),C(w),C(E)]})}function j(){return A(),null}export{j as FormBlocksRegistration,k as NewFormDialog,A as registerFormBlocks};
|
|
3
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/new-form-dialog.tsx","../src/register.tsx"],"sourcesContent":["'use client'\n\n/**\n * \"New form\" on the Forms list (W1): ask for a name, create the form, and land\n * on its canvas (W2).\n *\n * It sends the NAME and nothing else. The server gives a form created without\n * a tree its starting contact form, in the request's content language\n * (`formStarterTree` in `@murumets-ee/forms`) — so the browser shapes no part\n * of the tree, and an agent creating a form gets the same one.\n *\n * It posts to the generic entity API (`POST /api/admin/form`), where the\n * firewall sits: `form:create`, CSRF, and the site of the request (the\n * admin's patched `fetch` adds it).\n */\n\nimport { FORM_NAME_MAX } from '@murumets-ee/forms/schema'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n} from '@murumets-ee/ui'\nimport { useRouter } from 'next/navigation'\nimport { useTranslations } from 'next-intl'\nimport { type FormEvent, type ReactElement, useState } from 'react'\n\nexport interface NewFormDialogProps {\n readonly apiBasePath: string\n /** Where the form canvas lives — `<admin base>/forms`. */\n readonly formsBasePath: string\n}\n\nexport function NewFormDialog({ apiBasePath, formsBasePath }: NewFormDialogProps): ReactElement {\n const t = useTranslations('Forms.newDialog')\n const router = useRouter()\n const [open, setOpen] = useState(false)\n const [name, setName] = useState('')\n const [submitting, setSubmitting] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const trimmed = name.trim()\n const canSubmit = trimmed !== '' && trimmed.length <= FORM_NAME_MAX && !submitting\n\n const onSubmit = async (event: FormEvent) => {\n event.preventDefault()\n if (!canSubmit) return\n setSubmitting(true)\n setError(null)\n try {\n const res = await fetch(`${apiBasePath}/form`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ name: trimmed }),\n })\n if (!res.ok) throw new Error(t('failed', { status: res.status }))\n const created = (await res.json()) as { id?: unknown }\n if (typeof created.id !== 'string') throw new Error(t('noId'))\n setOpen(false)\n setSubmitting(false)\n router.push(`${formsBasePath}/${created.id}`)\n } catch (err) {\n setError(err instanceof Error ? err.message : t('failed', { status: 0 }))\n setSubmitting(false)\n }\n }\n\n return (\n <>\n <Button type=\"button\" size=\"sm\" onClick={() => setOpen(true)}>\n {t('title')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{t('title')}</DialogTitle>\n </DialogHeader>\n <form onSubmit={onSubmit} className=\"flex flex-col gap-3\">\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"new-form-name\">{t('name')}</Label>\n <Input\n id=\"new-form-name\"\n value={name}\n maxLength={FORM_NAME_MAX}\n onChange={(e) => setName(e.target.value)}\n placeholder={t('namePlaceholder')}\n aria-describedby=\"new-form-name-help\"\n />\n <p id=\"new-form-name-help\" className=\"text-xs text-muted-foreground\">\n {t('help')}\n </p>\n </div>\n {error !== null && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {error}\n </p>\n )}\n <div className=\"flex justify-end gap-2 pt-1\">\n <Button type=\"button\" variant=\"outline\" onClick={() => setOpen(false)}>\n {t('cancel')}\n </Button>\n <Button type=\"submit\" disabled={!canSubmit} loading={submitting}>\n {t('create')}\n </Button>\n </div>\n </form>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n","'use client'\n\n/**\n * Puts the `form` environment into the browser's block registry — the one\n * client-side step a canvas needs, MOUNTED by the admin layout beside\n * `<MenuBlocksRegistration />` (see CLAUDE.md's standing exception for why the\n * browser half is a registry call and not a `Plugin.contributes` slot).\n *\n * The server learns the same vocabulary declaratively from the plugin\n * (`blocks.blocks` + `blocks.environmentBases`), so the two agree on every\n * block name; this half adds what only the browser can hold — the renders and\n * the panel's two field editors.\n */\n\nimport {\n registerBlockEnvironment,\n registerBlockEnvironmentBase,\n registerBlockTheme,\n widenFieldType,\n} from '@murumets-ee/blocks/core'\nimport { formFieldKeyType, formPickerType, formSettingsTabsType } from '@murumets-ee/forms/editor'\nimport { FORMS_THEME, formsTheme } from '@murumets-ee/forms/render'\nimport {\n FORM_ENVIRONMENT,\n FORMS_SOURCE,\n formEnvironmentBlocks,\n formEnvironmentTraits,\n formSiteBlocks,\n PAGE_ENVIRONMENT,\n} from '@murumets-ee/forms/schema'\n\nexport function registerFormBlocks(): void {\n registerBlockTheme(formsTheme)\n registerBlockEnvironmentBase({\n name: FORM_ENVIRONMENT,\n blocks: formEnvironmentBlocks,\n traits: formEnvironmentTraits,\n fieldTypes: [widenFieldType(formFieldKeyType), widenFieldType(formSettingsTabsType)],\n defaultTheme: FORMS_THEME,\n })\n // The page canvas (D002): the Form block in the palette, and — because the\n // saved form it shows is drawn and edited IN PLACE on the page — every block\n // a form is made of, with their panel editors. A field is still offered only\n // inside a form (`ancestor`), so the page itself gains one entry: \"Form\".\n // The server's `page` environment holds the same blocks\n // (`formPageEnvironmentExtension`); the fields are edited in the form's own\n // document, judged against the `form` environment.\n registerBlockEnvironment({\n source: FORMS_SOURCE,\n name: PAGE_ENVIRONMENT,\n blocks: formSiteBlocks,\n fieldTypes: [\n widenFieldType(formPickerType),\n widenFieldType(formFieldKeyType),\n widenFieldType(formSettingsTabsType),\n ],\n })\n}\n\n/**\n * Render it, do not import it for effect: a component in the layout is what\n * guarantees the registration runs before any canvas resolves its vocabulary\n * (`resolveBlockVocabulary` runs during render). Same shape as menus-ui's.\n */\nexport function FormBlocksRegistration(): null {\n registerFormBlocks()\n return null\n}\n"],"mappings":"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"}
|
package/dist/pages.d.mts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PageProps } from "@murumets-ee/core";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/pages/form-canvas-page.d.ts
|
|
5
|
+
declare function FormCanvasPage({
|
|
6
|
+
params,
|
|
7
|
+
searchParams
|
|
8
|
+
}: PageProps<{
|
|
9
|
+
locale: string;
|
|
10
|
+
id: string;
|
|
11
|
+
}>): Promise<ReactNode>;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/pages/forms-list-page.d.ts
|
|
14
|
+
declare function FormsListPage({
|
|
15
|
+
params
|
|
16
|
+
}: PageProps<{
|
|
17
|
+
locale: string;
|
|
18
|
+
}>): Promise<ReactNode>;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { FormCanvasPage, FormsListPage };
|
|
21
|
+
//# sourceMappingURL=pages.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.d.mts","names":[],"sources":["../src/pages/form-canvas-page.tsx","../src/pages/forms-list-page.tsx"],"mappings":";;;;iBAqBsB,cAAA,CAAA;EACpB,MAAA;EACA;AAAA,GACC,SAAA;EAAY,MAAA;EAAgB,EAAA;AAAA,KAAgB,OAAA,CAAQ,SAAA;;;iBCSjC,aAAA,CAAA;EAAgB;AAAA,GAAU,SAAA;EAAY,MAAA;AAAA,KAAoB,OAAA,CAAQ,SAAA"}
|
package/dist/pages.mjs
ADDED
|
@@ -0,0 +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{FORM_ENTITY as i,Form as a}from"@murumets-ee/forms";import{notFound as o}from"next/navigation";import{fetchEntityList as s}from"@murumets-ee/admin-ui/server";import{WorkflowStatusBadge as c}from"@murumets-ee/admin-ui/workflow";import{countEmbedUsages as l}from"@murumets-ee/blocks/server";import{NewFormDialog as u}from"@murumets-ee/forms-ui";import{Table as d,TableBody as f,TableCell as p,TableHead as m,TableHeader as h,TableRow as g}from"@murumets-ee/ui";import _ from"next/link";import{getTranslations as v,setRequestLocale as y}from"next-intl/server";import{jsx as b,jsxs as x}from"react/jsx-runtime";const S=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function C({params:t,searchParams:n}){let{id:i}=await t;return S.test(i)||o(),r(a,e(),{params:t,searchParams:n})}async function w({params:r}){let S=e(),{locale:C}=await r;y(C);let w=await v(`Forms.list`),T=`${S.adminBasePath.replace(`:locale`,C)}/forms`;return S.withAdminContext(C,async()=>{await t(a,`view`,S)||o();let[{items:e,total:r},v]=await Promise.all([s(a,{sortField:`name`,sortDirection:`asc`,limit:200}),n(`form`,`create`,S)]),y=await l(i,e.map(e=>e.id)).catch(()=>null);return x(`div`,{className:`space-y-4 p-6`,children:[x(`div`,{className:`flex items-start justify-between gap-4`,children:[x(`div`,{children:[b(`h1`,{className:`text-2xl font-bold`,children:w(`title`)}),b(`p`,{className:`text-sm text-muted-foreground`,children:w(`description`)})]}),v&&b(u,{apiBasePath:S.apiBasePath??`/api/admin`,formsBasePath:T})]}),e.length===0?b(`p`,{className:`rounded-md border border-dashed p-6 text-sm text-muted-foreground`,children:w(`empty`)}):x(d,{children:[b(h,{children:x(g,{children:[b(m,{children:w(`name`)}),b(m,{children:w(`usedOn`)}),b(m,{className:`text-right`,children:w(`submissions`)}),b(m,{className:`text-right`,children:w(`new`)}),b(m,{className:`text-right`,children:w(`blocked`)}),b(m,{children:w(`status`)})]})}),b(f,{children:e.map(e=>x(g,{children:[b(p,{className:`font-medium`,children:b(_,{href:`${T}/${e.id}`,className:`text-primary hover:underline`,children:e.name})}),b(p,{className:`text-muted-foreground`,"data-forms-used-on":e.id,children:y===null?w(`notYet`):w(`usedOnPages`,{count:y.get(e.id)??0})}),b(p,{className:`text-right text-muted-foreground`,children:w(`notYet`)}),b(p,{className:`text-right text-muted-foreground`,children:w(`notYet`)}),b(p,{className:`text-right text-muted-foreground`,children:w(`notYet`)}),b(p,{children:b(c,{status:e.status===`published`?`published`:`draft`})})]},e.id))})]}),r>e.length&&b(`p`,{className:`text-xs text-muted-foreground`,children:w(`truncated`,{count:e.length})})]})})}export{C as FormCanvasPage,w as FormsListPage};
|
|
2
|
+
//# sourceMappingURL=pages.mjs.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { getFormsMessages } from "@murumets-ee/forms/i18n";
|
|
2
|
+
|
|
3
|
+
//#region ../admin-route/dist/index.d.mts
|
|
4
|
+
/**
|
|
5
|
+
* Spec passed to {@link defineFeature}.
|
|
6
|
+
*
|
|
7
|
+
* Plugin-level NON-CRUD resources whose permission grants aren't tied to a
|
|
8
|
+
* single route or entity — e.g. `ticketing.bulk-edit` with actions
|
|
9
|
+
* `['view', 'execute']`, or `commerce.imports` with `['view', 'export',
|
|
10
|
+
* 'cancel']`. Use `defineAdminRoute` for routes and `defineAdminPage` for
|
|
11
|
+
* pages; `defineFeature` is the catalog hook for everything that's neither.
|
|
12
|
+
*
|
|
13
|
+
* The resource string is the same shape as a `defineAdminRoute` permission's
|
|
14
|
+
* left side. Actions are the right side — one catalog entry is emitted per
|
|
15
|
+
* `(resource, action)` pair, exactly as if `defineAdminRoute` had been
|
|
16
|
+
* called once per action with a no-op handler.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* defineFeature({
|
|
21
|
+
* resource: 'ticketing.bulk-edit',
|
|
22
|
+
* actions: ['view', 'execute'],
|
|
23
|
+
* defaultRoles: ['admin', 'agent'],
|
|
24
|
+
* description: 'Operate ticketing bulk-edit tools',
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
interface DefineFeatureSpec {
|
|
29
|
+
/**
|
|
30
|
+
* Resource string — left side of the permission. Must be non-empty and
|
|
31
|
+
* not contain `:` (the action separator). Convention: dot-notation for
|
|
32
|
+
* plugin-prefixed names (`'ticketing.bulk-edit'`, `'commerce.imports'`).
|
|
33
|
+
*/
|
|
34
|
+
resource: string;
|
|
35
|
+
/**
|
|
36
|
+
* Actions on this resource. Each becomes a catalog entry
|
|
37
|
+
* `<resource>:<action>`. Must be non-empty; each action must be a
|
|
38
|
+
* non-empty string without `:`. Duplicates within the same call are
|
|
39
|
+
* rejected (would be silently deduped by `registerPermission` but the
|
|
40
|
+
* call-site shape suggests programmer error).
|
|
41
|
+
*/
|
|
42
|
+
actions: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Built-in roles granted EVERY action on this resource when the role row is
|
|
45
|
+
* first written, and granted the RESOURCE on a later boot if the saved row
|
|
46
|
+
* does not carry it yet.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ Not "on every `upsertBuiltInRoles`": its step 2 merges at RESOURCE
|
|
49
|
+
* granularity, so adding a new ACTION to a resource a role already holds
|
|
50
|
+
* reaches fresh installs and NOT existing ones. That asymmetry is deliberate
|
|
51
|
+
* — it is what stops an upgrade from undoing an operator's per-action edits —
|
|
52
|
+
* and it means a new action needs a migration or an operator, not a redeploy.
|
|
53
|
+
*
|
|
54
|
+
* ✅ **This is the field that actually seeds** — it is the only one. A
|
|
55
|
+
* `defineAdminRoute`/`defineAdminPage` `defaultRoles` carrying the same role
|
|
56
|
+
* names grants nothing (see their docblocks), so a permission that should be
|
|
57
|
+
* held by a non-admin role needs an entry HERE, reached declaratively via
|
|
58
|
+
* `Plugin.shared.features[]`. Read off the plugin objects rather than the
|
|
59
|
+
* catalog, because `registerPermission` keeps the first registrant's
|
|
60
|
+
* `source` and a permission registered by a route first would look like a
|
|
61
|
+
* route contribution afterwards (editor plan F111/R046).
|
|
62
|
+
*
|
|
63
|
+
* ⚠️ **Exactly three role names are honoured: `authenticated`, `editor`,
|
|
64
|
+
* `agent`** — `PLUGIN_SEEDABLE_ROLES` in `@murumets-ee/auth`. Anything else
|
|
65
|
+
* here is silently ignored, which is worth knowing BEFORE you write it:
|
|
66
|
+
*
|
|
67
|
+
* - `admin` is a bypass flag in `buildPermissionChecker`, not a row, and
|
|
68
|
+
* `PATCH /permissions` refuses to modify one — a grant would be redundant
|
|
69
|
+
* and uneditable. Most declarations name it and lose nothing by doing so.
|
|
70
|
+
* - `customer` must never reach `/api/admin/*` at all, per
|
|
71
|
+
* `buildInitialRoleDefinitions`' own docblock.
|
|
72
|
+
* - `public` is the anonymous visitor; changing what a site publishes is not
|
|
73
|
+
* a decision a plugin makes on an operator's behalf.
|
|
74
|
+
* - `viewer` is named in 20+ `defaultRoles` arrays repo-wide and is not in
|
|
75
|
+
* `BUILT_IN_ROLES`. It does not exist (editor plan F113).
|
|
76
|
+
*
|
|
77
|
+
* A permission that genuinely needs one of those roles wants a review, not an
|
|
78
|
+
* array entry. Ask for it.
|
|
79
|
+
*
|
|
80
|
+
* Defaults to `[]`. The same default-roles set applies to every action — if
|
|
81
|
+
* different actions need different default grants, register separate
|
|
82
|
+
* `defineFeature` calls.
|
|
83
|
+
*/
|
|
84
|
+
defaultRoles?: readonly string[];
|
|
85
|
+
/**
|
|
86
|
+
* Optional description for the Permission Matrix UI (PR-D) and audit
|
|
87
|
+
* logs. Plain-text, no markdown. Applied to every catalog entry the
|
|
88
|
+
* call produces; per-action descriptions need separate `defineFeature`
|
|
89
|
+
* calls (one per action).
|
|
90
|
+
*/
|
|
91
|
+
description?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Register a non-CRUD plugin resource in the permission catalog.
|
|
95
|
+
*
|
|
96
|
+
* **Plugin authors should NOT call this directly.** Contribute features
|
|
97
|
+
* declaratively via `Plugin.shared.features: DefineFeatureSpec[]` — the
|
|
98
|
+
* framework's merge step at boot iterates that array and calls this
|
|
99
|
+
* factory for each spec. The declarative path is the supported plugin-
|
|
100
|
+
* authoring API; direct invocation is reserved for tests / scripts /
|
|
101
|
+
* non-plugin call sites that need to populate the catalog explicitly.
|
|
102
|
+
*
|
|
103
|
+
* The returned `PermissionCatalogEntry[]` contains ONE entry per `(resource,
|
|
104
|
+
* action)` pair — the same shape every other catalog consumer (Permission
|
|
105
|
+
* Matrix UI, `buildResourceCatalog`) already understands. `upsertBuiltInRoles`
|
|
106
|
+
* is NOT among them: it reads `Plugin.shared.features[]` off the plugin
|
|
107
|
+
* objects, never the catalog, because `registerPermission` keeps the FIRST
|
|
108
|
+
* registrant's `source` and unions `defaultRoles` across all of them — so a
|
|
109
|
+
* feature registered by a route first is indistinguishable here from a route
|
|
110
|
+
* (editor plan R046). Each returned entry reflects the AUTHORITATIVE
|
|
111
|
+
* post-merge catalog state: on a fresh registration it equals the input
|
|
112
|
+
* spec's fields; on a repeat registration (something else already
|
|
113
|
+
* contributed the same `(resource, action)` pair) the entry carries the
|
|
114
|
+
* first registrant's `source` + `description` and the union of every
|
|
115
|
+
* registrant's `defaultRoles`. Callers consume the return value as a
|
|
116
|
+
* truthful snapshot, not the draft this call would have registered.
|
|
117
|
+
*
|
|
118
|
+
* Unlike {@link defineAdminRoute}, `defineFeature` does NOT produce a
|
|
119
|
+
* handler — its only contribution is the catalog registration. Use this
|
|
120
|
+
* for non-route, non-entity-action permissions that the toolkit's auth
|
|
121
|
+
* layer should know about: feature flags an admin can grant per-role,
|
|
122
|
+
* bulk operations that aren't a discrete HTTP endpoint, scheduled-job
|
|
123
|
+
* "run-now" capabilities, etc.
|
|
124
|
+
*
|
|
125
|
+
* **Why a wrapper exists when `registerPermission` already does the job:**
|
|
126
|
+
* the wrapper validates input shape (non-empty resource, non-empty
|
|
127
|
+
* actions, no `:` in either, no duplicate actions in the same call),
|
|
128
|
+
* splits a multi-action declaration into the right number of catalog
|
|
129
|
+
* entries, and sets `source: 'feature'` so the Matrix UI can group by
|
|
130
|
+
* declaration site. `registerPermission` is the lower-level primitive
|
|
131
|
+
* shared with `defineAdminRoute`; plugin code should reach for
|
|
132
|
+
* `defineFeature` instead.
|
|
133
|
+
*
|
|
134
|
+
* **Idempotency:** repeat calls for the same `(resource, action)` pair
|
|
135
|
+
* union their `defaultRoles` (same behavior as `defineAdminRoute`
|
|
136
|
+
* registering the same permission twice). Two plugins both declaring
|
|
137
|
+
* `'ticketing.bulk-edit:execute'` produce one catalog entry with the
|
|
138
|
+
* combined defaults.
|
|
139
|
+
*
|
|
140
|
+
* @example Plugin authoring (declarative — preferred):
|
|
141
|
+
* ```ts
|
|
142
|
+
* import type { Plugin } from '@murumets-ee/core'
|
|
143
|
+
*
|
|
144
|
+
* export function ticketingPlugin(): Plugin {
|
|
145
|
+
* return {
|
|
146
|
+
* name: '@app/ticketing',
|
|
147
|
+
* shared: {
|
|
148
|
+
* features: [
|
|
149
|
+
* {
|
|
150
|
+
* resource: 'ticketing.bulk-edit',
|
|
151
|
+
* actions: ['view', 'execute'],
|
|
152
|
+
* defaultRoles: ['admin', 'agent'],
|
|
153
|
+
* description: 'Operate ticketing bulk-edit tools',
|
|
154
|
+
* },
|
|
155
|
+
* ],
|
|
156
|
+
* },
|
|
157
|
+
* }
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @example Tests / scripts (direct invocation):
|
|
162
|
+
* ```ts
|
|
163
|
+
* import { defineFeature } from '@murumets-ee/core'
|
|
164
|
+
*
|
|
165
|
+
* const entries = defineFeature({
|
|
166
|
+
* resource: 'ticketing.bulk-edit',
|
|
167
|
+
* actions: ['view', 'execute'],
|
|
168
|
+
* defaultRoles: ['admin', 'agent'],
|
|
169
|
+
* })
|
|
170
|
+
* // entries[0].source === 'feature'
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/plugin.d.ts
|
|
175
|
+
declare function formsPlugin(): Pick<import("@murumets-ee/core").Plugin, "server" | "adminUi" | "postInstall" | "requires" | "contributes"> & {
|
|
176
|
+
readonly name: "@murumets-ee/forms";
|
|
177
|
+
readonly shared?: {
|
|
178
|
+
readonly features: readonly DefineFeatureSpec[];
|
|
179
|
+
readonly messages: typeof getFormsMessages;
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
//#endregion
|
|
183
|
+
export { formsPlugin };
|
|
184
|
+
//# sourceMappingURL=plugin.d.mts.map
|
|
@@ -0,0 +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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBCtwBc,WAAA,CAAA,GAAW,IAAA,6BAAA,MAAA;EAAA;;gCAAA,iBAAA;IAAA"}
|
package/dist/plugin.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{definePlugin as e}from"@murumets-ee/core";import{FORMS_MANAGE as t,Form as n,formsFeatures as r,formsRoutes as i}from"@murumets-ee/forms";import{getFormsMessages as a}from"@murumets-ee/forms/i18n";import{FormEmbedBlock as o,formEnvironment as s,formPageEnvironmentExtension as c,formSchemaBlocks as l}from"@murumets-ee/forms/schema";const u=async e=>(await import(`@murumets-ee/forms-ui/pages`)).FormsListPage(e),d=async e=>(await import(`@murumets-ee/forms-ui/pages`)).FormCanvasPage(e);function f(){return e({name:`@murumets-ee/forms`,requires:[`@murumets-ee/content`],server:{entities:[n],routes:[...i()]},contributes:{"blocks.blocks":[...l,o],"blocks.environmentBases":[s],"blocks.environments":c},shared:{features:r,messages:a},adminUi:{pages:{FormsListPage:u,FormCanvasPage:d},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{f as formsPlugin};
|
|
2
|
+
//# sourceMappingURL=plugin.mjs.map
|
|
@@ -0,0 +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 { FORMS_MANAGE, Form, formsFeatures, formsRoutes } 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.\n requires: ['@murumets-ee/content'],\n server: {\n entities: [Form],\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 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":"oVAqCA,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,qBAEN,SAAU,CAAC,sBAAsB,EACjC,OAAQ,CACN,SAAU,CAAC,CAAI,EAEf,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,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,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murumets-ee/forms-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.97.0",
|
|
4
|
+
"license": "Elastic-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Admin half of @murumets-ee/forms: the form canvas registration, the Forms list, the New form dialog and the plugin.",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.mts",
|
|
10
|
+
"import": "./dist/index.mjs"
|
|
11
|
+
},
|
|
12
|
+
"./pages": {
|
|
13
|
+
"types": "./dist/pages.d.mts",
|
|
14
|
+
"import": "./dist/pages.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./plugin": {
|
|
17
|
+
"types": "./dist/plugin.d.mts",
|
|
18
|
+
"import": "./dist/plugin.mjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@murumets-ee/admin-ui": "0.97.0",
|
|
26
|
+
"@murumets-ee/content": "0.97.0",
|
|
27
|
+
"@murumets-ee/forms": "0.97.0",
|
|
28
|
+
"@murumets-ee/core": "0.97.0",
|
|
29
|
+
"@murumets-ee/blocks": "0.97.0",
|
|
30
|
+
"@murumets-ee/ui": "0.97.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"next": ">=16.0.0 <17",
|
|
34
|
+
"next-intl": ">=4.0.0 <5",
|
|
35
|
+
"react": ">=19.0.0 <20",
|
|
36
|
+
"react-dom": ">=19.0.0 <20"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"next": {
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
42
|
+
"next-intl": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@testing-library/react": "^16.3.2",
|
|
48
|
+
"@types/react": "^19",
|
|
49
|
+
"@types/react-dom": "^19",
|
|
50
|
+
"happy-dom": "^20.10.1",
|
|
51
|
+
"next": "16.3.0",
|
|
52
|
+
"next-intl": "^4.13.0",
|
|
53
|
+
"react": "19.2.7",
|
|
54
|
+
"react-dom": "19.2.7",
|
|
55
|
+
"tsdown": "^0.22.2",
|
|
56
|
+
"typescript": "^5.7.3",
|
|
57
|
+
"vitest": "^2.1.8"
|
|
58
|
+
},
|
|
59
|
+
"typeCoverage": {
|
|
60
|
+
"atLeast": 100
|
|
61
|
+
},
|
|
62
|
+
"bundleBudget": {
|
|
63
|
+
"dist/index.mjs": 16384,
|
|
64
|
+
"dist/pages.mjs": 8192,
|
|
65
|
+
"dist/plugin.mjs": 5120
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsdown",
|
|
69
|
+
"dev": "tsdown --watch",
|
|
70
|
+
"test": "vitest run",
|
|
71
|
+
"test:watch": "vitest"
|
|
72
|
+
}
|
|
6
73
|
}
|
package/README.md
DELETED