@murumets-ee/forms-ui 0.99.0 → 0.102.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/canvas-header-action.d.mts +15 -0
- package/dist/canvas-header-action.d.mts.map +1 -0
- package/dist/canvas-header-action.mjs +3 -0
- package/dist/canvas-header-action.mjs.map +1 -0
- package/dist/index.d.mts +70 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/pages.d.mts +17 -1
- 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 +7 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/package.json +18 -11
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import { CanvasHeaderActionProps } from "@murumets-ee/admin-ui/content-editor";
|
|
3
|
+
import { ReactElement } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/canvas-header-action.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The words come from the editor's translator, so this entry imports no i18n
|
|
8
|
+
* library: `lumi.config.ts` reaches it under jiti, where `next-intl` cannot load.
|
|
9
|
+
*/
|
|
10
|
+
declare function FormSubmissionsHeaderAction({
|
|
11
|
+
t
|
|
12
|
+
}: CanvasHeaderActionProps): ReactElement | null;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { FormSubmissionsHeaderAction };
|
|
15
|
+
//# sourceMappingURL=canvas-header-action.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-header-action.d.mts","names":[],"sources":["../src/canvas-header-action.tsx"],"mappings":";;;;;;;;;iBAuBgB,2BAAA,CAAA;EAA8B;AAAA,GAAK,uBAAA,GAA0B,YAAA"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";import{submissionsHeaderFallback as e,useFormCanvasFacts as t}from"@murumets-ee/forms/canvas-facts";import{jsx as n}from"react/jsx-runtime";function r({t:r}){let i=t()?.submissions;if(i===void 0)return null;let a=r(`Forms.header.submissions`,e(i.fresh),{count:i.fresh});return n(`a`,{href:i.href,"data-form-submissions-link":``,className:`inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md px-2 font-medium text-[13px] text-muted-foreground transition-colors hover:bg-accent hover:text-foreground`,children:a})}export{r as FormSubmissionsHeaderAction};
|
|
3
|
+
//# sourceMappingURL=canvas-header-action.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-header-action.mjs","names":[],"sources":["../src/canvas-header-action.tsx"],"sourcesContent":["'use client'\n\n/**\n * \"Submissions (N new)\" in the form canvas HEADER, beside Publish — the form's\n * contribution to `Plugin.adminUi.canvasHeaderActions` (forms PR08, D011).\n *\n * A component REFERENCE shipped from its own `'use client'` subpath, because\n * the canvas is a client component and a closure cannot cross to it. The link\n * and the count come from the facts the canvas page provides, and the page\n * provides them ONLY to a person who holds `form_submissions:view` — so a role\n * without it gets no button, rather than one that answers \"not found\".\n */\n\nimport type { CanvasHeaderActionProps } from '@murumets-ee/admin-ui/content-editor'\nimport { submissionsHeaderFallback, useFormCanvasFacts } from '@murumets-ee/forms/canvas-facts'\nimport type { ReactElement } from 'react'\n\nconst KEY = 'Forms.header.submissions'\n\n/**\n * The words come from the editor's translator, so this entry imports no i18n\n * library: `lumi.config.ts` reaches it under jiti, where `next-intl` cannot load.\n */\nexport function FormSubmissionsHeaderAction({ t }: CanvasHeaderActionProps): ReactElement | null {\n const submissions = useFormCanvasFacts()?.submissions\n if (submissions === undefined) return null\n const label = t(KEY, submissionsHeaderFallback(submissions.fresh), { count: submissions.fresh })\n return (\n <a\n href={submissions.href}\n data-form-submissions-link=\"\"\n className=\"inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md px-2 font-medium text-[13px] text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n >\n {label}\n </a>\n )\n}\n"],"mappings":";yJAuBA,SAAgB,EAA4B,CAAE,KAAmD,CAC/F,IAAM,EAAc,EAAmB,CAAC,EAAE,YAC1C,GAAI,IAAgB,IAAA,GAAW,OAAO,KACtC,IAAM,EAAQ,EAAE,2BAAK,EAA0B,EAAY,KAAK,EAAG,CAAE,MAAO,EAAY,KAAM,CAAC,EAC/F,OACE,EAAC,IAAD,CACE,KAAM,EAAY,KAClB,6BAA2B,GAC3B,UAAU,+KAET,CACA,CAAA,CAEP"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
|
|
2
2
|
import { ReactElement } from "react";
|
|
3
|
+
import { DeliveryRow, SubmissionColumn, SubmissionRow } from "@murumets-ee/forms";
|
|
3
4
|
|
|
5
|
+
//#region src/delete-form-dialog.d.ts
|
|
6
|
+
interface DeleteFormButtonProps {
|
|
7
|
+
readonly apiBasePath: string;
|
|
8
|
+
readonly formId: string;
|
|
9
|
+
readonly formName: string;
|
|
10
|
+
/** Stored submissions, or `null` when this person may not see the count. */
|
|
11
|
+
readonly submissions: number | null;
|
|
12
|
+
/** Published pages showing the form, or `null` when the count could not be read. */
|
|
13
|
+
readonly pages: number | null;
|
|
14
|
+
}
|
|
15
|
+
declare function DeleteFormButton({
|
|
16
|
+
apiBasePath,
|
|
17
|
+
formId,
|
|
18
|
+
formName,
|
|
19
|
+
submissions,
|
|
20
|
+
pages
|
|
21
|
+
}: DeleteFormButtonProps): ReactElement;
|
|
22
|
+
//#endregion
|
|
4
23
|
//#region src/new-form-dialog.d.ts
|
|
5
24
|
interface NewFormDialogProps {
|
|
6
25
|
readonly apiBasePath: string;
|
|
@@ -12,6 +31,23 @@ declare function NewFormDialog({
|
|
|
12
31
|
formsBasePath
|
|
13
32
|
}: NewFormDialogProps): ReactElement;
|
|
14
33
|
//#endregion
|
|
34
|
+
//#region src/privacy-view.d.ts
|
|
35
|
+
interface PrivacyRequestsViewProps {
|
|
36
|
+
readonly apiBasePath: string;
|
|
37
|
+
/** `<admin base>/forms` — a listed submission links to its form's Submissions. */
|
|
38
|
+
readonly formsBasePath: string;
|
|
39
|
+
/** The interface language, for the export's column words and delimiter. */
|
|
40
|
+
readonly locale: string;
|
|
41
|
+
/** May erase (`form_submissions:manage`). */
|
|
42
|
+
readonly canErase: boolean;
|
|
43
|
+
}
|
|
44
|
+
declare function PrivacyRequestsView({
|
|
45
|
+
apiBasePath,
|
|
46
|
+
formsBasePath,
|
|
47
|
+
locale,
|
|
48
|
+
canErase
|
|
49
|
+
}: PrivacyRequestsViewProps): ReactElement;
|
|
50
|
+
//#endregion
|
|
15
51
|
//#region src/register.d.ts
|
|
16
52
|
declare function registerFormBlocks(): void;
|
|
17
53
|
/**
|
|
@@ -21,5 +57,38 @@ declare function registerFormBlocks(): void;
|
|
|
21
57
|
*/
|
|
22
58
|
declare function FormBlocksRegistration(): null;
|
|
23
59
|
//#endregion
|
|
24
|
-
|
|
60
|
+
//#region src/submissions-view.d.ts
|
|
61
|
+
interface SubmissionsViewProps {
|
|
62
|
+
readonly formId: string;
|
|
63
|
+
readonly rows: readonly SubmissionRow[];
|
|
64
|
+
readonly columns: readonly SubmissionColumn[];
|
|
65
|
+
/** May mark read / archive / delete (`form_submissions:manage`). */
|
|
66
|
+
readonly canManage: boolean;
|
|
67
|
+
/** Each listed submission's emails (notification / auto-reply), by submission id. */
|
|
68
|
+
readonly deliveries: Readonly<Record<string, readonly DeliveryRow[]>>;
|
|
69
|
+
readonly apiBasePath: string;
|
|
70
|
+
/** What to say when no submission is listed — the page knows whether a filter is why. */
|
|
71
|
+
readonly emptyText: string;
|
|
72
|
+
}
|
|
73
|
+
declare function SubmissionsView({
|
|
74
|
+
formId,
|
|
75
|
+
rows,
|
|
76
|
+
columns,
|
|
77
|
+
canManage,
|
|
78
|
+
deliveries,
|
|
79
|
+
apiBasePath,
|
|
80
|
+
emptyText
|
|
81
|
+
}: SubmissionsViewProps): ReactElement;
|
|
82
|
+
/**
|
|
83
|
+
* "Export CSV" — a plain download link to the export of the filters on screen.
|
|
84
|
+
* A link cannot carry the site header the admin's own requests send, so the
|
|
85
|
+
* selected site is added as `_site` when it is clicked, from the page URL.
|
|
86
|
+
*/
|
|
87
|
+
declare function SubmissionsExportLink({
|
|
88
|
+
href
|
|
89
|
+
}: {
|
|
90
|
+
readonly href: string;
|
|
91
|
+
}): ReactElement;
|
|
92
|
+
//#endregion
|
|
93
|
+
export { DeleteFormButton, type DeleteFormButtonProps, FormBlocksRegistration, NewFormDialog, type NewFormDialogProps, PrivacyRequestsView, type PrivacyRequestsViewProps, SubmissionsExportLink, SubmissionsView, type SubmissionsViewProps, registerFormBlocks };
|
|
25
94
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/new-form-dialog.tsx","../src/register.tsx"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/delete-form-dialog.tsx","../src/new-form-dialog.tsx","../src/privacy-view.tsx","../src/register.tsx","../src/submissions-view.tsx"],"mappings":";;;;;UAwBiB,qBAAA;EAAA,SACN,WAAA;EAAA,SACA,MAAA;EAAA,SACA,QAAA;EAH2B;EAAA,SAK3B,WAAA;EAL2B;EAAA,SAO3B,KAAA;AAAA;AAAA,iBAGK,gBAAA,CAAA;EACd,WAAA;EACA,MAAA;EACA,QAAA;EACA,WAAA;EACA;AAAA,GACC,qBAAA,GAAwB,YAAA;;;UCVV,kBAAA;EAAA,SACN,WAAA;;WAEA,aAAa;AAAA;AAAA,iBAGR,aAAA,CAAA;EAAgB,WAAA;EAAa;AAAA,GAAiB,kBAAA,GAAqB,YAAA;;;UCDlE,wBAAA;EAAA,SACN,WAAA;;WAEA,aAAA;EFd2B;EAAA,SEgB3B,MAAA;EFhB2B;EAAA,SEkB3B,QAAA;AAAA;AAAA,iBAsCK,mBAAA,CAAA;EACd,WAAA;EACA,aAAA;EACA,MAAA;EACA;AAAA,GACC,wBAAA,GAA2B,YAAA;;;iBCtDd,kBAAA,CAAA;;;;AHPhB;;iBGwCgB,sBAAA,CAAA;;;UClBC,oBAAA;EAAA,SACN,MAAA;EAAA,SACA,IAAA,WAAe,aAAA;EAAA,SACf,OAAA,WAAkB,gBAAA;;WAElB,SAAA;EJ1BA;EAAA,SI4BA,UAAA,EAAY,QAAA,CAAS,MAAA,kBAAwB,WAAA;EAAA,SAC7C,WAAA;EJzBA;EAAA,SI2BA,SAAA;AAAA;AAAA,iBAwDK,eAAA,CAAA;EACd,MAAA;EACA,IAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,WAAA;EACA;AAAA,GACC,oBAAA,GAAuB,YAAA;AJtF1B;;;;;AAAA,iBIqiBgB,qBAAA,CAAA;EAAwB;AAAA;EAAA,SAAmB,IAAA;AAAA,IAAiB,YAAY"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
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};
|
|
2
|
+
import{Badge as e,Button as t,Checkbox as n,Dialog as r,DialogContent as i,DialogDescription as a,DialogHeader as o,DialogTitle as ee,Input as s,Label as c,Table as l,TableBody as u,TableCell as d,TableHead as f,TableHeader as p,TableRow as m}from"@murumets-ee/ui";import{useRouter as h}from"next/navigation";import{useFormatter as g,useTranslations as _}from"next-intl";import{useEffect as v,useId as y,useRef as b,useState as x,useTransition as S}from"react";import{Fragment as C,jsx as w,jsxs as T}from"react/jsx-runtime";import{FORMS_SOURCE as E,FORM_ENVIRONMENT as D,FORM_NAME_MAX as O,PAGE_ENVIRONMENT as k,formEnvironmentBlocks as A,formEnvironmentTraits as j,formSiteBlocks as M}from"@murumets-ee/forms/schema";import{registerBlockEnvironment as N,registerBlockEnvironmentBase as P,registerBlockTheme as F,widenFieldType as I}from"@murumets-ee/blocks/core";import{formFieldKeyType as L,formPickerType as R,formSettingsTabsType as z}from"@murumets-ee/forms/editor";import{FORMS_THEME as B,formsTheme as V}from"@murumets-ee/forms/render";import{withAdminSiteParam as H}from"@murumets-ee/admin-ui";function U({apiBasePath:e,formId:n,formName:s,submissions:c,pages:l}){let u=_(`Forms.remove`),d=_(`Forms.list`),f=h(),[,p]=S(),[m,g]=x(!1),[v,y]=x(!1),[b,E]=x(null),[D,O]=x(!1),k=l!==null&&l>0,A=()=>{g(!1),D&&p(()=>f.refresh())},j=async()=>{y(!0),E(null);try{let t=await fetch(`${e}/form-remove`,{method:`DELETE`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:n})});if(t.status===202){E(u(`awaitingApproval`));return}if(t.status===409){E(u(`stillInUse`));return}if(t.status===403){E(u(`forbidden`));return}let r=await t.json().catch(()=>null),i=typeof r?.submissions==`number`?r.submissions:0;if(t.status===500&&r?.deleted===!0){O(!0),E(u(`lateFailed`));return}if(p(()=>f.refresh()),t.status===500&&r?.more===!0){E(u(`partial`,{count:i}));return}if(!t.ok){E(u(`failed`,{status:t.status}));return}if(r?.deleted!==!0){E(u(`more`,{count:i}));return}g(!1)}catch{E(u(`failed`,{status:0}))}finally{y(!1)}};return T(C,{children:[w(t,{type:`button`,variant:`ghost`,size:`sm`,className:`text-destructive hover:text-destructive`,"aria-label":d(`deleteForm`,{form:s}),onClick:()=>{E(null),g(!0)},children:d(`delete`)}),w(r,{open:m,onOpenChange:e=>e?g(!0):A(),children:T(i,{className:`max-w-md`,children:[w(o,{children:w(ee,{children:u(`title`,{form:s})})}),w(a,{asChild:!0,children:w(`div`,{className:`space-y-2 text-sm text-muted-foreground`,children:k?w(`p`,{"data-forms-remove-in-use":``,children:u(`inUse`,{count:l})}):T(C,{children:[w(`p`,{"data-forms-remove-count":c??``,children:c===null?u(`submissionsUnknown`):u(`submissions`,{count:c})}),w(`p`,{children:u(`trash`)})]})})}),b!==null&&w(`p`,{role:`alert`,className:`text-sm text-destructive`,children:b}),T(`div`,{className:`flex justify-end gap-2 pt-2`,children:[w(t,{type:`button`,variant:`outline`,onClick:A,children:u(`cancel`)}),!k&&!D&&w(t,{variant:`destructive`,disabled:v,onClick:()=>void j(),children:u(c===0?`confirmEmpty`:`confirm`)})]})]})})]})}function W({apiBasePath:e,formsBasePath:n}){let a=_(`Forms.newDialog`),l=h(),[u,d]=x(!1),[f,p]=x(``),[m,g]=x(!1),[v,y]=x(null),b=f.trim(),S=b!==``&&b.length<=O&&!m;return T(C,{children:[w(t,{type:`button`,size:`sm`,onClick:()=>d(!0),children:a(`title`)}),w(r,{open:u,onOpenChange:d,children:T(i,{className:`max-w-md`,children:[w(o,{children:w(ee,{children:a(`title`)})}),T(`form`,{onSubmit:async t=>{if(t.preventDefault(),S){g(!0),y(null);try{let t=await fetch(`${e}/form`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:b})});if(!t.ok)throw Error(a(`failed`,{status:t.status}));let r=await t.json();if(typeof r.id!=`string`)throw Error(a(`noId`));d(!1),g(!1),l.push(`${n}/${r.id}`)}catch(e){y(e instanceof Error?e.message:a(`failed`,{status:0})),g(!1)}}},className:`flex flex-col gap-3`,children:[T(`div`,{className:`flex flex-col gap-1.5`,children:[w(c,{htmlFor:`new-form-name`,children:a(`name`)}),w(s,{id:`new-form-name`,value:f,maxLength:O,onChange:e=>p(e.target.value),placeholder:a(`namePlaceholder`),"aria-describedby":`new-form-name-help`}),w(`p`,{id:`new-form-name-help`,className:`text-xs text-muted-foreground`,children:a(`help`)})]}),v!==null&&w(`p`,{role:`alert`,className:`text-sm text-destructive`,children:v}),T(`div`,{className:`flex justify-end gap-2 pt-1`,children:[w(t,{type:`button`,variant:`outline`,onClick:()=>d(!1),children:a(`cancel`)}),w(t,{type:`submit`,disabled:!S,loading:m,children:a(`create`)})]})]})]})})]})}function G(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function K(e){return G(e)&&typeof e.id==`string`&&typeof e.formId==`string`&&(typeof e.formName==`string`||e.formName===null)&&typeof e.createdAt==`string`&&(typeof e.pagePath==`string`||e.pagePath===null)}function te(e,t){return/filename="([^"]+)"/.exec(e.headers.get(`Content-Disposition`)??``)?.[1]??t}function q({apiBasePath:e,formsBasePath:n,locale:h,canErase:v}){let b=_(`Forms.privacy`),S=g(),E=y(),[D,O]=x(``),[k,A]=x(null),[j,M]=x(!1),[N,P]=x(!1),[F,I]=x(null),[L,R]=x(null),z=`${e}/form-privacy`,B=(e,t,n)=>fetch(`${z}${e}`,{method:t,headers:{"Content-Type":`application/json`},body:JSON.stringify(n)}),V=e=>e.ok?!0:(e.status===400?I(b(`invalid`)):e.status===403?I(b(`forbidden`)):e.status===503?I(b(`busy`)):I(b(`failed`,{status:e.status})),!1),H=async e=>{M(!0),I(null);try{let t=await B(`/search`,`POST`,{email:e});if(!V(t)){A(null);return}let n=await t.json().catch(()=>null);if(!G(n)||typeof n.total!=`number`||!Array.isArray(n.items)){I(b(`failed`,{status:t.status})),A(null);return}let r=n.items;A({email:e,total:n.total,items:r.filter(K),unindexed:typeof n.unindexed==`number`?n.unindexed:0,exportMax:typeof n.exportMax==`number`?n.exportMax:5e3})}catch{I(b(`failed`,{status:0})),A(null)}finally{M(!1)}},U=e=>{e.preventDefault();let t=D.trim();if(R(null),t===``){I(b(`invalid`)),A(null);return}H(t)},W=async e=>{if(k!==null){M(!0),I(null),R(null);try{let t=await B(`/export`,`POST`,{email:k.email,format:e,uiLocale:h});if(!V(t))return;let n=URL.createObjectURL(await t.blob()),r=document.createElement(`a`);r.href=n,r.download=te(t,`privacy-request.${e}`),document.body.append(r),r.click(),r.remove(),setTimeout(()=>URL.revokeObjectURL(n),6e4),t.headers.get(`X-Lumi-Export-Truncated`)===`true`&&R(b(`exportCapped`,{max:k.exportMax}))}catch{I(b(`failed`,{status:0}))}finally{M(!1)}}},q=async()=>{if(k!==null){P(!1),M(!0),I(null),R(null);try{let e=await B(``,`DELETE`,{email:k.email}),t=await e.json().catch(()=>null),n=G(t)&&typeof t.erased==`number`?t.erased:null;if(e.status===500&&n!==null&&G(t)&&t.more===!0){await H(k.email),I(b(`eraseFailed`,{count:n}));return}if(!V(e))return;if(n===null){I(b(`failed`,{status:e.status}));return}let r=G(t)&&t.more===!0;await H(k.email),R(b(r?`erasedMore`:`erased`,{count:n}))}catch{I(b(`failed`,{status:0}))}finally{M(!1)}}},J=e=>S.dateTime(new Date(e),{dateStyle:`short`,timeStyle:`short`});return T(`div`,{className:`space-y-4`,children:[T(`form`,{onSubmit:U,className:`flex flex-wrap items-end gap-3 rounded-md border p-3`,autoComplete:`off`,children:[T(`div`,{className:`flex min-w-[16rem] flex-1 flex-col gap-1.5`,children:[w(c,{htmlFor:E,children:b(`email`)}),w(s,{id:E,type:`email`,inputMode:`email`,autoComplete:`off`,spellCheck:!1,maxLength:320,value:D,onChange:e=>O(e.target.value),placeholder:b(`placeholder`)})]}),w(t,{type:`submit`,disabled:j,children:b(j&&k===null?`finding`:`find`)})]}),F!==null&&w(`p`,{role:`alert`,className:`rounded-md border border-destructive/40 p-3 text-sm`,children:F}),L!==null&&w(`p`,{role:`status`,className:`rounded-md border p-3 text-sm`,"data-forms-privacy-notice":``,children:L}),k!==null&&k.unindexed>0&&w(`p`,{role:`status`,className:`rounded-md border border-amber-500/50 bg-amber-500/10 p-3 text-sm`,"data-forms-privacy-unindexed":k.unindexed,children:b(`unindexed`,{count:k.unindexed})}),k!==null&&k.total===0&&T(`div`,{className:`rounded-md border border-dashed p-6 text-sm text-muted-foreground`,children:[w(`p`,{children:b(`none`)}),w(`p`,{className:`mt-1`,children:b(`noneHint`)})]}),k!==null&&k.total>0&&T(C,{children:[w(`div`,{className:`overflow-x-auto rounded-md border`,children:T(l,{children:[w(p,{children:T(m,{children:[w(f,{children:b(`form`)}),w(f,{children:b(`received`)}),w(f,{children:b(`page`)})]})}),w(u,{children:k.items.map(e=>T(m,{"data-forms-privacy-row":e.id,children:[w(d,{className:`font-medium`,children:e.formName===null?w(`span`,{className:`text-muted-foreground`,children:b(`deletedForm`)}):w(`a`,{href:`${n}/${e.formId}/submissions`,className:`text-primary hover:underline`,children:e.formName})}),w(d,{className:`whitespace-nowrap tabular-nums`,children:J(e.createdAt)}),w(d,{className:`text-muted-foreground`,children:e.pagePath??`—`})]},e.id))})]})}),T(`div`,{className:`flex flex-wrap items-center gap-2 text-sm`,children:[w(`span`,{"data-forms-privacy-total":k.total,children:b(`summary`,{count:k.total})}),w(t,{variant:`outline`,size:`sm`,disabled:j,onClick:()=>void W(`csv`),children:b(`exportCsv`)}),w(t,{variant:`outline`,size:`sm`,disabled:j,onClick:()=>void W(`json`),children:b(`exportJson`)}),v&&w(t,{variant:`outline`,size:`sm`,className:`border-destructive/50 text-destructive hover:bg-destructive/10`,disabled:j,onClick:()=>P(!0),children:b(`eraseAll`,{count:k.total})})]}),k.total>k.items.length&&w(`p`,{className:`text-xs text-muted-foreground`,children:b(`listCapped`,{shown:k.items.length,max:k.exportMax})}),w(`p`,{className:`text-xs text-muted-foreground`,children:b(`eraseNote`)})]}),w(r,{open:N,onOpenChange:P,children:T(i,{className:`max-w-md`,children:[w(o,{children:w(ee,{children:b(`confirmTitle`,{count:Math.max(k?.total??1,1)})})}),w(a,{children:b(`confirmBody`)}),T(`div`,{className:`flex justify-end gap-2 pt-2`,children:[w(t,{variant:`outline`,onClick:()=>P(!1),children:b(`cancel`)}),w(t,{variant:`destructive`,disabled:j,onClick:()=>void q(),children:b(`confirmErase`)})]})]})})]})}function J(){F(V),P({name:D,blocks:A,traits:j,fieldTypes:[I(L),I(z)],defaultTheme:B}),N({source:E,name:k,blocks:M,fieldTypes:[I(R),I(L),I(z)]})}function ne(){return J(),null}const re={new:`default`,read:`secondary`,archived:`outline`},Y=[`no_recipients`,`disabled`],ie=(e,t)=>e.status===`failed`&&!(e.kind===`autoReply`&&e.errorClass===`timeout`&&t-new Date(e.updatedAt).getTime()<36e5)||e.status===`queued`&&t-new Date(e.updatedAt).getTime()>3e5||e.status===`skipped`&&e.errorClass!==null&&Y.includes(e.errorClass),ae=e=>e===`failed`?`destructive`:e===`sent`?`secondary`:`outline`;function X(e,t){return Object.hasOwn(e,t)?e[t]:void 0}function oe(e,t){let n=t.map(e=>e.key).filter(t=>Object.hasOwn(e.data,t));return[...n,...Object.keys(e.data).filter(e=>!n.includes(e))]}function Z({formId:s,rows:c,columns:y,canManage:C,deliveries:E,apiBasePath:D,emptyText:O}){let k=_(`Forms.submissions`),A=_(`Forms.submissions.emails`),j=_(`Forms.submissionStatus`),M=g(),N=h(),[P,F]=S(),[I,L]=x(null),R=I?.id??null,z=b(null),B=b(new Set),[V,H]=x(new Set),[U,W]=x(null),[G,K]=x(!1),[te,q]=x(null),[J,ne]=x(null),Y=c.find(e=>e.id===R)??I,Z=c.filter(e=>V.has(e.id)).map(e=>e.id),se=c.length>0&&Z.length===c.length,ce=e=>M.dateTime(new Date(e),{dateStyle:`short`,timeStyle:`short`}),le=e=>e===void 0||e===``?`—`:typeof e==`boolean`?k(e?`yes`:`no`):String(e),ue=async(e,t)=>{K(!0),q(null);try{let n=await fetch(`${D}/form-submissions`,{method:e,headers:{"Content-Type":`application/json`},body:JSON.stringify({form:s,...t})});if(n.status===202)return q(k(`awaitingApproval`)),!1;if(!n.ok)return q(k(`failed`,{status:n.status})),!1;let r=await n.json().catch(()=>null),i=e===`DELETE`?r?.deleted:r?.changed;if(F(()=>N.refresh()),typeof i!=`number`||i<=0){q(k(`nothingChanged`));let e=t.ids;return Array.isArray(e)&&L(t=>t&&e.includes(t.id)?null:t),!1}return!0}catch{return q(k(`failed`,{status:0})),!1}finally{K(!1)}},de=async(e,t)=>{K(!0),q(null);try{let n=await fetch(`${D}/form-emails/retry`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({form:s,submission:e,kind:t})});if(n.status===202){q(k(`awaitingApproval`));return}if(!n.ok){q(k(`failed`,{status:n.status}));return}if((await n.json().catch(()=>null))?.queued!==!0){q(A(`notQueued`));return}ne(A(`retrying`)),F(()=>N.refresh())}catch{q(k(`failed`,{status:0}))}finally{K(!1)}},Q=async(e,t)=>{let n=await ue(`PATCH`,{ids:e,status:t});if(!n||t===`new`)for(let t of e)B.current.delete(t);return n&&L(n=>n&&e.includes(n.id)?{...n,status:t}:n),n},fe=e=>{L(e),C&&e.status===`new`&&!B.current.has(e.id)&&(B.current.add(e.id),Q([e.id],`read`))};v(()=>{R!==null&&z.current?.focus()},[R]);let pe=async()=>{if(U===null)return;let e=U;W(null),await ue(`DELETE`,{ids:e})&&(H(t=>new Set([...t].filter(t=>!e.includes(t)))),L(t=>t&&e.includes(t.id)?null:t))},me=(e,t)=>H(n=>{let r=new Set(n);return t?r.add(e):r.delete(e),r}),$=G||P,he=Date.now(),ge=e=>A.has(`reason.${e}`)?A(`reason.${e}`):e,_e=t=>{if(t===void 0||t.length===0)return w(`span`,{className:`text-muted-foreground`,children:`—`});let n=t.find(e=>e.status===`failed`)??t.find(e=>e.status===`queued`)??t.find(e=>e.status===`sent`)??t[0];return n===void 0?w(`span`,{className:`text-muted-foreground`,children:`—`}):w(e,{variant:ae(n.status),children:A(`delivery.${n.status}`)})};return T(`div`,{className:`space-y-3`,children:[J!==null&&w(`p`,{role:`status`,className:`rounded-md border p-3 text-sm`,children:J}),te!==null&&w(`p`,{role:`alert`,className:`rounded-md border border-destructive/40 p-3 text-sm`,children:te}),T(`div`,{className:`flex flex-col gap-4 lg:flex-row lg:items-start`,children:[T(`div`,{className:`min-w-0 flex-1 space-y-3`,children:[c.length===0&&w(`p`,{className:`rounded-md border border-dashed p-6 text-sm text-muted-foreground`,children:O}),w(`div`,{className:`overflow-x-auto rounded-md border`,hidden:c.length===0,children:T(l,{children:[w(p,{children:T(m,{children:[C&&w(f,{className:`w-10`,children:w(n,{"aria-label":k(`selectAll`),checked:se,onCheckedChange:e=>H(e===!0?new Set(c.map(e=>e.id)):new Set)})}),w(f,{className:`whitespace-nowrap`,children:k(`received`)}),w(f,{className:`whitespace-nowrap`,children:k(`status`)}),w(f,{className:`whitespace-nowrap`,children:A(`emailsColumn`)}),w(f,{className:`whitespace-nowrap`,children:k(`page`)}),y.map(e=>T(f,{"data-forms-column":e.key,title:e.label,className:`max-w-[14rem] truncate whitespace-nowrap`,children:[e.label,e.retired&&T(`span`,{className:`ml-1 text-xs font-normal text-muted-foreground`,children:[`(`,k(`removedField`),`)`]})]},e.key))]})}),w(u,{children:c.map(t=>T(m,{"data-state":t.id===R?`selected`:void 0,className:t.status===`new`?`font-medium`:void 0,children:[C&&w(d,{children:w(n,{"aria-label":k(`selectRow`,{when:ce(t.createdAt)}),checked:V.has(t.id),onCheckedChange:e=>me(t.id,e===!0)})}),w(d,{className:`whitespace-nowrap`,children:w(`button`,{type:`button`,className:`text-primary hover:underline`,"aria-expanded":t.id===R,onClick:()=>fe(t),children:ce(t.createdAt)})}),w(d,{children:w(e,{variant:re[t.status],children:j(t.status)})}),w(d,{"data-forms-emails-cell":``,children:_e(X(E,t.id))}),w(d,{className:`text-muted-foreground`,children:t.pagePath??`—`}),y.map(e=>{let n=le(X(t.data,e.key));return w(d,{className:`max-w-[16rem] truncate`,children:n.length>60?`${n.slice(0,60)}…`:n},e.key)})]},t.id))})]})}),C&&c.length>0&&w(t,{variant:`outline`,size:`sm`,disabled:$||Z.length===0,onClick:()=>W(Z),children:k(`deleteSelected`,{count:Z.length})})]}),Y!==null&&T(`aside`,{ref:z,tabIndex:-1,"aria-label":k(`detail`),className:`w-full shrink-0 space-y-4 rounded-md border p-4 lg:w-96`,children:[T(`div`,{className:`flex items-start justify-between gap-2`,children:[T(`div`,{children:[w(`p`,{className:`font-medium`,children:ce(Y.createdAt)}),w(`p`,{className:`text-sm text-muted-foreground`,children:Y.pagePath===null?k(`noPage`):k(`fromPage`,{page:Y.pagePath})})]}),w(t,{variant:`ghost`,size:`sm`,onClick:()=>L(null),children:k(`close`)})]}),w(`dl`,{className:`space-y-3 text-sm`,children:oe(Y,y).map(e=>{let t=X(Y.data,e),n=X(Y.consent,e);return T(`div`,{children:[w(`dt`,{className:`text-xs text-muted-foreground`,children:X(Y.fields,e)?.label??e}),w(`dd`,{className:`whitespace-pre-wrap break-words`,children:le(t)}),n!==void 0&&w(`dd`,{className:`mt-1 text-xs text-muted-foreground`,children:k(`consentWording`,{wording:n})})]},e)})}),T(`section`,{className:`space-y-2 border-t pt-3 text-sm`,"aria-label":A(`emailsTitle`),children:[w(`h3`,{className:`text-xs font-medium text-muted-foreground`,children:A(`emailsTitle`)}),(X(E,Y.id)??[]).length===0?w(`p`,{className:`text-muted-foreground`,children:A(`noEmails`)}):w(`ul`,{className:`space-y-2`,children:(X(E,Y.id)??[]).map(n=>T(`li`,{"data-forms-delivery":`${n.kind}:${n.status}`,children:[T(`div`,{className:`flex flex-wrap items-center gap-2`,children:[w(`span`,{className:`font-medium`,children:A(`kind.${n.kind}`)}),w(e,{variant:ae(n.status),children:A(`delivery.${n.status}`)}),C&&ie(n,he)&&w(t,{variant:`outline`,size:`sm`,disabled:$,onClick:()=>void de(Y.id,n.kind),children:A(`retry`)})]}),n.errorClass!==null&&w(`p`,{className:`mt-1 text-xs text-muted-foreground`,children:ge(n.errorClass)})]},n.kind))})]}),C&&T(`div`,{className:`flex flex-wrap gap-2 border-t pt-3`,children:[Y.status===`new`?w(t,{variant:`outline`,size:`sm`,disabled:$,onClick:()=>void Q([Y.id],`read`),children:k(`markRead`)}):w(t,{variant:`outline`,size:`sm`,disabled:$,onClick:()=>void Q([Y.id],`new`),children:k(`markNew`)}),Y.status===`archived`?w(t,{variant:`outline`,size:`sm`,disabled:$,onClick:()=>void Q([Y.id],`read`),children:k(`unarchive`)}):w(t,{variant:`outline`,size:`sm`,disabled:$,onClick:()=>void Q([Y.id],`archived`),children:k(`archive`)}),w(t,{variant:`destructive`,size:`sm`,disabled:$,onClick:()=>W([Y.id]),children:k(`delete`)})]})]})]}),w(r,{open:U!==null,onOpenChange:e=>!e&&W(null),children:T(i,{children:[w(o,{children:w(ee,{children:k(`confirmDeleteTitle`,{count:U?.length??0})})}),w(a,{children:k(`confirmDeleteBody`)}),T(`div`,{className:`flex justify-end gap-2`,children:[w(t,{variant:`outline`,onClick:()=>W(null),children:k(`cancel`)}),w(t,{variant:`destructive`,disabled:G,onClick:()=>void pe(),children:k(`confirmDelete`)})]})]})})]})}function se({href:e}){return w(t,{variant:`outline`,size:`sm`,asChild:!0,children:w(`a`,{href:e,download:!0,onPointerDown:t=>{t.currentTarget.href=H(e)},onFocus:t=>{t.currentTarget.href=H(e)},onClick:t=>{t.currentTarget.href=H(e)},children:_(`Forms.submissions`)(`exportCsv`)})})}export{U as DeleteFormButton,ne as FormBlocksRegistration,W as NewFormDialog,q as PrivacyRequestsView,se as SubmissionsExportLink,Z as SubmissionsView,J as registerFormBlocks};
|
|
3
3
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/delete-form-dialog.tsx","../src/new-form-dialog.tsx","../src/privacy-view.tsx","../src/register.tsx","../src/submissions-view.tsx"],"sourcesContent":["'use client'\n\n/**\n * \"Delete…\" on a row of the Forms list (W1): say what goes with the form —\n * how many submissions, for good — and delete both on confirm (forms D007).\n *\n * It calls `DELETE /api/admin/form-remove`, not the generic entity delete:\n * that one would move the form to the Trash and leave its visitors' answers\n * stored where nobody can see them. The numbers shown are the list's own, read\n * on the server; the route checks everything again and is what decides.\n */\n\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '@murumets-ee/ui'\nimport { useRouter } from 'next/navigation'\nimport { useTranslations } from 'next-intl'\nimport { type ReactElement, useState, useTransition } from 'react'\n\nexport interface DeleteFormButtonProps {\n readonly apiBasePath: string\n readonly formId: string\n readonly formName: string\n /** Stored submissions, or `null` when this person may not see the count. */\n readonly submissions: number | null\n /** Published pages showing the form, or `null` when the count could not be read. */\n readonly pages: number | null\n}\n\nexport function DeleteFormButton({\n apiBasePath,\n formId,\n formName,\n submissions,\n pages,\n}: DeleteFormButtonProps): ReactElement {\n const t = useTranslations('Forms.remove')\n const tList = useTranslations('Forms.list')\n const router = useRouter()\n const [, startRefresh] = useTransition()\n const [open, setOpen] = useState(false)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [reloadOnClose, setReloadOnClose] = useState(false)\n const inUse = pages !== null && pages > 0\n\n /** Close the dialog — and reload the list if an answer was held back for it. */\n const close = (): void => {\n setOpen(false)\n if (reloadOnClose) startRefresh(() => router.refresh())\n }\n\n const remove = async (): Promise<void> => {\n setBusy(true)\n setError(null)\n try {\n const res = await fetch(`${apiBasePath}/form-remove`, {\n method: 'DELETE',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id: formId }),\n })\n if (res.status === 202) {\n // An agent's delete was parked for a person to approve.\n setError(t('awaitingApproval'))\n return\n }\n if (res.status === 409) {\n setError(t('stillInUse'))\n return\n }\n if (res.status === 403) {\n setError(t('forbidden'))\n return\n }\n const answer = (await res.json().catch(() => null)) as {\n deleted?: unknown\n more?: unknown\n submissions?: unknown\n } | null\n const gone = typeof answer?.submissions === 'number' ? answer.submissions : 0\n if (res.status === 500 && answer?.deleted === true) {\n // The form went, but a submission sent while it was going could not\n // be removed. This dialog lives in the form's row, which a reload\n // takes away — so the list is reloaded when the person closes it.\n setReloadOnClose(true)\n setError(t('lateFailed'))\n return\n }\n // Whatever happened, the list's numbers are stale now.\n startRefresh(() => router.refresh())\n if (res.status === 500 && answer?.more === true) {\n // A batch failed part-way. The form was kept, so this row — and this\n // dialog — are still here to ask again from.\n setError(t('partial', { count: gone }))\n return\n }\n if (!res.ok) {\n setError(t('failed', { status: res.status }))\n return\n }\n if (answer?.deleted !== true) {\n // More submissions than one request removes: the form was kept.\n setError(t('more', { count: gone }))\n return\n }\n setOpen(false)\n } catch {\n setError(t('failed', { status: 0 }))\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-destructive hover:text-destructive\"\n aria-label={tList('deleteForm', { form: formName })}\n onClick={() => {\n setError(null)\n setOpen(true)\n }}\n >\n {tList('delete')}\n </Button>\n <Dialog open={open} onOpenChange={(next) => (next ? setOpen(true) : close())}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{t('title', { form: formName })}</DialogTitle>\n </DialogHeader>\n <DialogDescription asChild>\n <div className=\"space-y-2 text-sm text-muted-foreground\">\n {inUse ? (\n <p data-forms-remove-in-use=\"\">{t('inUse', { count: pages })}</p>\n ) : (\n <>\n <p data-forms-remove-count={submissions ?? ''}>\n {submissions === null\n ? t('submissionsUnknown')\n : t('submissions', { count: submissions })}\n </p>\n <p>{t('trash')}</p>\n </>\n )}\n </div>\n </DialogDescription>\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-2\">\n <Button type=\"button\" variant=\"outline\" onClick={close}>\n {t('cancel')}\n </Button>\n {!inUse && !reloadOnClose && (\n <Button variant=\"destructive\" disabled={busy} onClick={() => void remove()}>\n {submissions === 0 ? t('confirmEmpty') : t('confirm')}\n </Button>\n )}\n </div>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n","'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 * Privacy requests (W5) — the interactive half: enter an address, see which\n * submissions carry it, export them, erase them.\n *\n * The address is kept in this component's state and sent in request BODIES\n * only (`privacy-routes.ts` says why): it is never put in the URL, so it does\n * not reach browser history or a server log, and reloading the page starts\n * empty. Nothing is fetched until a person presses Find.\n *\n * What is listed is where and when — form, time, page — not what was said;\n * the answers are in the export, and on each form's own Submissions screen.\n */\n\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@murumets-ee/ui'\nimport { useFormatter, useTranslations } from 'next-intl'\nimport { type FormEvent, type ReactElement, useId, useState } from 'react'\nimport type { PrivacyMatch } from './privacy-match.js'\n\nexport interface PrivacyRequestsViewProps {\n readonly apiBasePath: string\n /** `<admin base>/forms` — a listed submission links to its form's Submissions. */\n readonly formsBasePath: string\n /** The interface language, for the export's column words and delimiter. */\n readonly locale: string\n /** May erase (`form_submissions:manage`). */\n readonly canErase: boolean\n}\n\ninterface Found {\n /** The address the list is FOR — what export and erase name, whatever the box says now. */\n readonly email: string\n readonly total: number\n readonly items: readonly PrivacyMatch[]\n /** Submissions of this site the search could NOT look through yet. */\n readonly unindexed: number\n /** Most submissions one export holds. */\n readonly exportMax: number\n}\n\n/** What the screen says an export holds when the server did not say — the server's own cap. */\nconst EXPORT_MAX_FALLBACK = 5000\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction isMatch(value: unknown): value is PrivacyMatch {\n return (\n isRecord(value) &&\n typeof value.id === 'string' &&\n typeof value.formId === 'string' &&\n (typeof value.formName === 'string' || value.formName === null) &&\n typeof value.createdAt === 'string' &&\n (typeof value.pagePath === 'string' || value.pagePath === null)\n )\n}\n\n/** The file name a download answers with, or `fallback`. */\nfunction downloadName(res: Response, fallback: string): string {\n const match = /filename=\"([^\"]+)\"/.exec(res.headers.get('Content-Disposition') ?? '')\n return match?.[1] ?? fallback\n}\n\nexport function PrivacyRequestsView({\n apiBasePath,\n formsBasePath,\n locale,\n canErase,\n}: PrivacyRequestsViewProps): ReactElement {\n const t = useTranslations('Forms.privacy')\n const format = useFormatter()\n const inputId = useId()\n const [email, setEmail] = useState('')\n const [found, setFound] = useState<Found | null>(null)\n const [busy, setBusy] = useState(false)\n const [confirming, setConfirming] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [notice, setNotice] = useState<string | null>(null)\n\n const endpoint = `${apiBasePath}/form-privacy`\n\n const send = (path: string, method: 'POST' | 'DELETE', body: Record<string, unknown>) =>\n fetch(`${endpoint}${path}`, {\n method,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n\n /** Words for a refusal; `true` when the answer was a success. */\n const explain = (res: Response): boolean => {\n if (res.ok) return true\n if (res.status === 400) setError(t('invalid'))\n else if (res.status === 403) setError(t('forbidden'))\n else if (res.status === 503) setError(t('busy'))\n else setError(t('failed', { status: res.status }))\n return false\n }\n\n const find = async (address: string): Promise<void> => {\n setBusy(true)\n setError(null)\n try {\n const res = await send('/search', 'POST', { email: address })\n if (!explain(res)) {\n setFound(null)\n return\n }\n const answer: unknown = await res.json().catch(() => null)\n if (!isRecord(answer) || typeof answer.total !== 'number' || !Array.isArray(answer.items)) {\n setError(t('failed', { status: res.status }))\n setFound(null)\n return\n }\n const items: unknown[] = answer.items\n setFound({\n email: address,\n total: answer.total,\n items: items.filter(isMatch),\n unindexed: typeof answer.unindexed === 'number' ? answer.unindexed : 0,\n exportMax: typeof answer.exportMax === 'number' ? answer.exportMax : EXPORT_MAX_FALLBACK,\n })\n } catch {\n setError(t('failed', { status: 0 }))\n setFound(null)\n } finally {\n setBusy(false)\n }\n }\n\n const onSubmit = (event: FormEvent) => {\n event.preventDefault()\n const address = email.trim()\n setNotice(null)\n // The server judges the address; this only saves a round trip for an empty box.\n if (address === '') {\n setError(t('invalid'))\n setFound(null)\n return\n }\n void find(address)\n }\n\n const download = async (kind: 'csv' | 'json'): Promise<void> => {\n if (found === null) return\n setBusy(true)\n setError(null)\n setNotice(null)\n try {\n const res = await send('/export', 'POST', {\n email: found.email,\n format: kind,\n uiLocale: locale,\n })\n if (!explain(res)) return\n const url = URL.createObjectURL(await res.blob())\n const link = document.createElement('a')\n link.href = url\n link.download = downloadName(res, `privacy-request.${kind}`)\n document.body.append(link)\n link.click()\n link.remove()\n // Not at once: some browsers cancel a download whose URL is revoked in\n // the same tick it was clicked.\n setTimeout(() => URL.revokeObjectURL(url), 60_000)\n if (res.headers.get('X-Lumi-Export-Truncated') === 'true') {\n setNotice(t('exportCapped', { max: found.exportMax }))\n }\n } catch {\n setError(t('failed', { status: 0 }))\n } finally {\n setBusy(false)\n }\n }\n\n const erase = async (): Promise<void> => {\n if (found === null) return\n setConfirming(false)\n setBusy(true)\n setError(null)\n setNotice(null)\n try {\n const res = await send('', 'DELETE', { email: found.email })\n const answer: unknown = await res.json().catch(() => null)\n const erased = isRecord(answer) && typeof answer.erased === 'number' ? answer.erased : null\n // A batch failed part-way and submissions remain: the answer still says\n // how many went.\n if (res.status === 500 && erased !== null && isRecord(answer) && answer.more === true) {\n await find(found.email)\n setError(t('eraseFailed', { count: erased }))\n return\n }\n if (!explain(res)) return\n if (erased === null) {\n setError(t('failed', { status: res.status }))\n return\n }\n const more = isRecord(answer) && answer.more === true\n // Show what is left as the server now finds it — nothing, normally.\n await find(found.email)\n setNotice(more ? t('erasedMore', { count: erased }) : t('erased', { count: erased }))\n } catch {\n setError(t('failed', { status: 0 }))\n } finally {\n setBusy(false)\n }\n }\n\n const when = (iso: string) =>\n format.dateTime(new Date(iso), { dateStyle: 'short', timeStyle: 'short' })\n\n return (\n <div className=\"space-y-4\">\n <form\n onSubmit={onSubmit}\n className=\"flex flex-wrap items-end gap-3 rounded-md border p-3\"\n // The address must not be offered back by the browser on the next visit.\n autoComplete=\"off\"\n >\n <div className=\"flex min-w-[16rem] flex-1 flex-col gap-1.5\">\n <Label htmlFor={inputId}>{t('email')}</Label>\n <Input\n id={inputId}\n type=\"email\"\n inputMode=\"email\"\n autoComplete=\"off\"\n spellCheck={false}\n maxLength={320}\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n placeholder={t('placeholder')}\n />\n </div>\n <Button type=\"submit\" disabled={busy}>\n {busy && found === null ? t('finding') : t('find')}\n </Button>\n </form>\n\n {error !== null && (\n <p role=\"alert\" className=\"rounded-md border border-destructive/40 p-3 text-sm\">\n {error}\n </p>\n )}\n {notice !== null && (\n <p role=\"status\" className=\"rounded-md border p-3 text-sm\" data-forms-privacy-notice=\"\">\n {notice}\n </p>\n )}\n\n {found !== null && found.unindexed > 0 && (\n <p\n role=\"status\"\n className=\"rounded-md border border-amber-500/50 bg-amber-500/10 p-3 text-sm\"\n data-forms-privacy-unindexed={found.unindexed}\n >\n {t('unindexed', { count: found.unindexed })}\n </p>\n )}\n\n {found !== null && found.total === 0 && (\n <div className=\"rounded-md border border-dashed p-6 text-sm text-muted-foreground\">\n <p>{t('none')}</p>\n <p className=\"mt-1\">{t('noneHint')}</p>\n </div>\n )}\n\n {found !== null && found.total > 0 && (\n <>\n <div className=\"overflow-x-auto rounded-md border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{t('form')}</TableHead>\n <TableHead>{t('received')}</TableHead>\n <TableHead>{t('page')}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {found.items.map((item) => (\n <TableRow key={item.id} data-forms-privacy-row={item.id}>\n <TableCell className=\"font-medium\">\n {item.formName === null ? (\n <span className=\"text-muted-foreground\">{t('deletedForm')}</span>\n ) : (\n <a\n href={`${formsBasePath}/${item.formId}/submissions`}\n className=\"text-primary hover:underline\"\n >\n {item.formName}\n </a>\n )}\n </TableCell>\n <TableCell className=\"whitespace-nowrap tabular-nums\">\n {when(item.createdAt)}\n </TableCell>\n <TableCell className=\"text-muted-foreground\">{item.pagePath ?? '—'}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2 text-sm\">\n <span data-forms-privacy-total={found.total}>\n {t('summary', { count: found.total })}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n onClick={() => void download('csv')}\n >\n {t('exportCsv')}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n onClick={() => void download('json')}\n >\n {t('exportJson')}\n </Button>\n {canErase && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"border-destructive/50 text-destructive hover:bg-destructive/10\"\n disabled={busy}\n onClick={() => setConfirming(true)}\n >\n {t('eraseAll', { count: found.total })}\n </Button>\n )}\n </div>\n {found.total > found.items.length && (\n <p className=\"text-xs text-muted-foreground\">\n {t('listCapped', { shown: found.items.length, max: found.exportMax })}\n </p>\n )}\n <p className=\"text-xs text-muted-foreground\">{t('eraseNote')}</p>\n </>\n )}\n\n <Dialog open={confirming} onOpenChange={setConfirming}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {t('confirmTitle', { count: Math.max(found?.total ?? 1, 1) })}\n </DialogTitle>\n </DialogHeader>\n <DialogDescription>{t('confirmBody')}</DialogDescription>\n <div className=\"flex justify-end gap-2 pt-2\">\n <Button variant=\"outline\" onClick={() => setConfirming(false)}>\n {t('cancel')}\n </Button>\n <Button variant=\"destructive\" disabled={busy} onClick={() => void erase()}>\n {t('confirmErase')}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n </div>\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","'use client'\n\n/**\n * The interactive half of the Submissions screen (W4): the table with a\n * column per field, the detail panel beside it, and the actions — mark read,\n * archive, delete, delete selected, export.\n *\n * The rows come from the server page as props (no fetch on mount). An action\n * calls the `form-submissions` routes and then asks the server for the page\n * again (`router.refresh()`), so what is on screen is always what is stored.\n *\n * Opening a new submission marks it read — that is what \"new\" means — but only\n * for someone who may change submissions; a read-only viewer leaves no trace.\n *\n * The detail panel shows each answer under the label the field had WHEN IT WAS\n * SENT (the row's own snapshot), while a column header carries the field's\n * current label: a renamed field keeps old answers readable both ways.\n */\n\nimport { withAdminSiteParam } from '@murumets-ee/admin-ui'\nimport type {\n DeliveryRow,\n SubmissionColumn,\n SubmissionRow,\n SubmissionStatus,\n} from '@murumets-ee/forms'\nimport {\n Badge,\n Button,\n Checkbox,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@murumets-ee/ui'\nimport { useRouter } from 'next/navigation'\nimport { useFormatter, useTranslations } from 'next-intl'\nimport { type ReactElement, useEffect, useRef, useState, useTransition } from 'react'\n\nexport interface SubmissionsViewProps {\n readonly formId: string\n readonly rows: readonly SubmissionRow[]\n readonly columns: readonly SubmissionColumn[]\n /** May mark read / archive / delete (`form_submissions:manage`). */\n readonly canManage: boolean\n /** Each listed submission's emails (notification / auto-reply), by submission id. */\n readonly deliveries: Readonly<Record<string, readonly DeliveryRow[]>>\n readonly apiBasePath: string\n /** What to say when no submission is listed — the page knows whether a filter is why. */\n readonly emptyText: string\n}\n\n/** Longest answer shown in a table cell; the panel shows it whole. */\nconst CELL_MAX = 60\n\nconst STATUS_VARIANT: Record<SubmissionStatus, 'default' | 'secondary' | 'outline'> = {\n new: 'default',\n read: 'secondary',\n archived: 'outline',\n}\n\n/**\n * A Retry only where trying again can change the answer: a failed send, or an\n * email skipped because a setting that has since been changed was missing.\n * Not one refused for the hourly limit, a bad address or a vanished form —\n * those would be refused again.\n */\nconst RETRYABLE_SKIPS: readonly string[] = ['no_recipients', 'disabled']\n/** A `queued` email this old has lost its job (worker down, process died) — it can be queued again. */\nconst STUCK_AFTER_MS = 5 * 60 * 1000\n/**\n * A timed-out auto-reply keeps its hourly slot on purpose (the provider may\n * still deliver it), so a Retry inside that hour would only be refused.\n */\nconst GUARD_WINDOW_MS = 60 * 60 * 1000\nconst canRetry = (row: DeliveryRow, now: number): boolean =>\n (row.status === 'failed' &&\n !(\n row.kind === 'autoReply' &&\n row.errorClass === 'timeout' &&\n now - new Date(row.updatedAt).getTime() < GUARD_WINDOW_MS\n )) ||\n (row.status === 'queued' && now - new Date(row.updatedAt).getTime() > STUCK_AFTER_MS) ||\n (row.status === 'skipped' && row.errorClass !== null && RETRYABLE_SKIPS.includes(row.errorClass))\n\nconst deliveryVariant = (\n status: DeliveryRow['status'],\n): 'default' | 'secondary' | 'outline' | 'destructive' =>\n status === 'failed' ? 'destructive' : status === 'sent' ? 'secondary' : 'outline'\n\n/** `record[key]` when it is the record's OWN — a field keyed `constructor` must not read the prototype. */\nfunction own<T>(record: Readonly<Record<string, T>>, key: string): T | undefined {\n return Object.hasOwn(record, key) ? record[key] : undefined\n}\n\n/**\n * The keys of one submission's answers in the order the form asks them: the\n * list's columns first (current fields, then retired ones), then anything the\n * row holds that no column names. Stored JSON keeps no order of its own.\n */\nfunction answerKeys(row: SubmissionRow, columns: readonly SubmissionColumn[]): string[] {\n const ordered = columns.map((column) => column.key).filter((key) => Object.hasOwn(row.data, key))\n return [...ordered, ...Object.keys(row.data).filter((key) => !ordered.includes(key))]\n}\n\nexport function SubmissionsView({\n formId,\n rows,\n columns,\n canManage,\n deliveries,\n apiBasePath,\n emptyText,\n}: SubmissionsViewProps): ReactElement {\n const t = useTranslations('Forms.submissions')\n const tEmails = useTranslations('Forms.submissions.emails')\n const tStatus = useTranslations('Forms.submissionStatus')\n const format = useFormatter()\n const router = useRouter()\n const [refreshing, startRefresh] = useTransition()\n // The submission in the panel, as it was when opened. Kept as a row rather\n // than an id: marking it read (or archiving it) can take it OUT of the\n // filtered list, and the person is still reading it.\n const [opened, setOpened] = useState<SubmissionRow | null>(null)\n const openId = opened?.id ?? null\n const panel = useRef<HTMLElement>(null)\n /** New rows already asked to be marked read — one request each, however fast the clicks. */\n const marked = useRef(new Set<string>())\n const [selected, setSelected] = useState<ReadonlySet<string>>(new Set())\n const [confirming, setConfirming] = useState<readonly string[] | null>(null)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [notice, setNotice] = useState<string | null>(null)\n\n // The list's own copy when the row is still listed (its status is current),\n // else the copy taken when it was opened. Selection is derived the same way:\n // a row that has left the list is simply no longer selected.\n const open = rows.find((row) => row.id === openId) ?? opened\n const selectedIds = rows.filter((row) => selected.has(row.id)).map((row) => row.id)\n const allSelected = rows.length > 0 && selectedIds.length === rows.length\n\n const when = (iso: string) =>\n format.dateTime(new Date(iso), { dateStyle: 'short', timeStyle: 'short' })\n\n const answer = (value: SubmissionRow['data'][string] | undefined): string => {\n if (value === undefined || value === '') return '—'\n if (typeof value === 'boolean') return value ? t('yes') : t('no')\n return String(value)\n }\n\n const call = async (method: 'PATCH' | 'DELETE', body: Record<string, unknown>) => {\n setBusy(true)\n setError(null)\n try {\n const res = await fetch(`${apiBasePath}/form-submissions`, {\n method,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ form: formId, ...body }),\n })\n if (res.status === 202) {\n // An agent's delete was parked for a person to approve.\n setError(t('awaitingApproval'))\n return false\n }\n if (!res.ok) {\n setError(t('failed', { status: res.status }))\n return false\n }\n // The route answers how many rows it touched. None means the rows are\n // gone or were never this form's — say so rather than look successful.\n const answer = (await res.json().catch(() => null)) as {\n changed?: unknown\n deleted?: unknown\n } | null\n const touched = method === 'DELETE' ? answer?.deleted : answer?.changed\n startRefresh(() => router.refresh())\n // Success is a positive count. Anything else — zero, or an answer that\n // carries no count at all — is told to the person, never assumed.\n if (typeof touched !== 'number' || touched <= 0) {\n setError(t('nothingChanged'))\n // Whatever was named is not there any more: do not keep it open.\n const named: unknown = body.ids\n if (Array.isArray(named)) {\n setOpened((current) => (current && named.includes(current.id) ? null : current))\n }\n return false\n }\n return true\n } catch {\n setError(t('failed', { status: 0 }))\n return false\n } finally {\n setBusy(false)\n }\n }\n\n const retryEmail = async (submission: string, kind: DeliveryRow['kind']) => {\n setBusy(true)\n setError(null)\n try {\n const res = await fetch(`${apiBasePath}/form-emails/retry`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ form: formId, submission, kind }),\n })\n if (res.status === 202) {\n // An agent's request, parked for a person to approve.\n setError(t('awaitingApproval'))\n return\n }\n if (!res.ok) {\n setError(t('failed', { status: res.status }))\n return\n }\n // The route answers 200 even when the queue could not take it.\n const answer = (await res.json().catch(() => null)) as { queued?: unknown } | null\n if (answer?.queued !== true) {\n setError(tEmails('notQueued'))\n return\n }\n setNotice(tEmails('retrying'))\n startRefresh(() => router.refresh())\n } catch {\n setError(t('failed', { status: 0 }))\n } finally {\n setBusy(false)\n }\n }\n\n const setStatus = async (ids: readonly string[], status: SubmissionStatus) => {\n const done = await call('PATCH', { ids, status })\n // A row that is new again, or whose mark-read did not happen, is marked\n // read the next time it is opened.\n if (!done || status === 'new') for (const id of ids) marked.current.delete(id)\n // Keep the panel's copy in step when its row is no longer in the list.\n if (done)\n setOpened((current) =>\n current && ids.includes(current.id) ? { ...current, status } : current,\n )\n return done\n }\n\n const openRow = (row: SubmissionRow) => {\n setOpened(row)\n if (canManage && row.status === 'new' && !marked.current.has(row.id)) {\n marked.current.add(row.id)\n void setStatus([row.id], 'read')\n }\n }\n\n // Moving focus is the one thing here React cannot express: when a panel\n // opens, a keyboard or screen-reader user is taken to it.\n useEffect(() => {\n if (openId !== null) panel.current?.focus()\n }, [openId])\n\n const confirmDelete = async () => {\n if (confirming === null) return\n const ids = confirming\n setConfirming(null)\n if (await call('DELETE', { ids })) {\n setSelected((current) => new Set([...current].filter((id) => !ids.includes(id))))\n // A deleted submission is not left open.\n setOpened((current) => (current && ids.includes(current.id) ? null : current))\n }\n }\n\n const toggle = (id: string, on: boolean) =>\n setSelected((current) => {\n const next = new Set(current)\n if (on) next.add(id)\n else next.delete(id)\n return next\n })\n\n const disabled = busy || refreshing\n // Read once per render: only the \"stuck\" test of a queued email needs it.\n const nowMs = Date.now()\n\n /** Why an email failed or was not sent, in words — an unknown class is shown as it is. */\n const reasonText = (errorClass: string): string =>\n tEmails.has(`reason.${errorClass}`) ? tEmails(`reason.${errorClass}`) : errorClass\n\n /** One chip per submission: the worst thing that happened to its emails. */\n const emailsBadge = (rows: readonly DeliveryRow[] | undefined): ReactElement => {\n if (rows === undefined || rows.length === 0)\n return <span className=\"text-muted-foreground\">—</span>\n const worst =\n rows.find((r) => r.status === 'failed') ??\n rows.find((r) => r.status === 'queued') ??\n rows.find((r) => r.status === 'sent') ??\n rows[0]\n if (worst === undefined) return <span className=\"text-muted-foreground\">—</span>\n return (\n <Badge variant={deliveryVariant(worst.status)}>{tEmails(`delivery.${worst.status}`)}</Badge>\n )\n }\n\n return (\n <div className=\"space-y-3\">\n {notice !== null && (\n <p role=\"status\" className=\"rounded-md border p-3 text-sm\">\n {notice}\n </p>\n )}\n {error !== null && (\n <p role=\"alert\" className=\"rounded-md border border-destructive/40 p-3 text-sm\">\n {error}\n </p>\n )}\n\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start\">\n <div className=\"min-w-0 flex-1 space-y-3\">\n {rows.length === 0 && (\n <p className=\"rounded-md border border-dashed p-6 text-sm text-muted-foreground\">\n {emptyText}\n </p>\n )}\n <div className=\"overflow-x-auto rounded-md border\" hidden={rows.length === 0}>\n <Table>\n <TableHeader>\n <TableRow>\n {canManage && (\n <TableHead className=\"w-10\">\n <Checkbox\n aria-label={t('selectAll')}\n checked={allSelected}\n onCheckedChange={(value) =>\n setSelected(\n value === true ? new Set(rows.map((row) => row.id)) : new Set(),\n )\n }\n />\n </TableHead>\n )}\n <TableHead className=\"whitespace-nowrap\">{t('received')}</TableHead>\n <TableHead className=\"whitespace-nowrap\">{t('status')}</TableHead>\n <TableHead className=\"whitespace-nowrap\">{tEmails('emailsColumn')}</TableHead>\n <TableHead className=\"whitespace-nowrap\">{t('page')}</TableHead>\n {columns.map((column) => (\n // One line, however long the label (a consent's is a\n // sentence): the whole of it is the cell's title.\n <TableHead\n key={column.key}\n data-forms-column={column.key}\n title={column.label}\n className=\"max-w-[14rem] truncate whitespace-nowrap\"\n >\n {column.label}\n {column.retired && (\n <span className=\"ml-1 text-xs font-normal text-muted-foreground\">\n ({t('removedField')})\n </span>\n )}\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {rows.map((row) => (\n <TableRow\n key={row.id}\n data-state={row.id === openId ? 'selected' : undefined}\n className={row.status === 'new' ? 'font-medium' : undefined}\n >\n {canManage && (\n <TableCell>\n <Checkbox\n aria-label={t('selectRow', { when: when(row.createdAt) })}\n checked={selected.has(row.id)}\n onCheckedChange={(value) => toggle(row.id, value === true)}\n />\n </TableCell>\n )}\n <TableCell className=\"whitespace-nowrap\">\n <button\n type=\"button\"\n className=\"text-primary hover:underline\"\n aria-expanded={row.id === openId}\n onClick={() => openRow(row)}\n >\n {when(row.createdAt)}\n </button>\n </TableCell>\n <TableCell>\n <Badge variant={STATUS_VARIANT[row.status]}>{tStatus(row.status)}</Badge>\n </TableCell>\n <TableCell data-forms-emails-cell=\"\">\n {emailsBadge(own(deliveries, row.id))}\n </TableCell>\n <TableCell className=\"text-muted-foreground\">{row.pagePath ?? '—'}</TableCell>\n {columns.map((column) => {\n const text = answer(own(row.data, column.key))\n return (\n <TableCell key={column.key} className=\"max-w-[16rem] truncate\">\n {text.length > CELL_MAX ? `${text.slice(0, CELL_MAX)}…` : text}\n </TableCell>\n )\n })}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n\n {canManage && rows.length > 0 && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={disabled || selectedIds.length === 0}\n onClick={() => setConfirming(selectedIds)}\n >\n {t('deleteSelected', { count: selectedIds.length })}\n </Button>\n )}\n </div>\n\n {open !== null && (\n <aside\n ref={panel}\n tabIndex={-1}\n aria-label={t('detail')}\n className=\"w-full shrink-0 space-y-4 rounded-md border p-4 lg:w-96\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div>\n <p className=\"font-medium\">{when(open.createdAt)}</p>\n <p className=\"text-sm text-muted-foreground\">\n {open.pagePath === null ? t('noPage') : t('fromPage', { page: open.pagePath })}\n </p>\n </div>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setOpened(null)}>\n {t('close')}\n </Button>\n </div>\n\n <dl className=\"space-y-3 text-sm\">\n {answerKeys(open, columns).map((key) => {\n const value = own(open.data, key)\n const wording = own(open.consent, key)\n return (\n <div key={key}>\n {/* The label the field had when this was sent — the row's own snapshot. */}\n <dt className=\"text-xs text-muted-foreground\">\n {own(open.fields, key)?.label ?? key}\n </dt>\n <dd className=\"whitespace-pre-wrap break-words\">{answer(value)}</dd>\n {wording !== undefined && (\n <dd className=\"mt-1 text-xs text-muted-foreground\">\n {t('consentWording', { wording })}\n </dd>\n )}\n </div>\n )\n })}\n </dl>\n\n <section\n className=\"space-y-2 border-t pt-3 text-sm\"\n aria-label={tEmails('emailsTitle')}\n >\n <h3 className=\"text-xs font-medium text-muted-foreground\">\n {tEmails('emailsTitle')}\n </h3>\n {(own(deliveries, open.id) ?? []).length === 0 ? (\n <p className=\"text-muted-foreground\">{tEmails('noEmails')}</p>\n ) : (\n <ul className=\"space-y-2\">\n {(own(deliveries, open.id) ?? []).map((row) => (\n <li key={row.kind} data-forms-delivery={`${row.kind}:${row.status}`}>\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium\">{tEmails(`kind.${row.kind}`)}</span>\n <Badge variant={deliveryVariant(row.status)}>\n {tEmails(`delivery.${row.status}`)}\n </Badge>\n {canManage && canRetry(row, nowMs) && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={disabled}\n onClick={() => void retryEmail(open.id, row.kind)}\n >\n {tEmails('retry')}\n </Button>\n )}\n </div>\n {row.errorClass !== null && (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {reasonText(row.errorClass)}\n </p>\n )}\n </li>\n ))}\n </ul>\n )}\n </section>\n\n {canManage && (\n <div className=\"flex flex-wrap gap-2 border-t pt-3\">\n {open.status === 'new' ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={disabled}\n onClick={() => void setStatus([open.id], 'read')}\n >\n {t('markRead')}\n </Button>\n ) : (\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={disabled}\n onClick={() => void setStatus([open.id], 'new')}\n >\n {t('markNew')}\n </Button>\n )}\n {open.status === 'archived' ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={disabled}\n onClick={() => void setStatus([open.id], 'read')}\n >\n {t('unarchive')}\n </Button>\n ) : (\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={disabled}\n onClick={() => void setStatus([open.id], 'archived')}\n >\n {t('archive')}\n </Button>\n )}\n <Button\n variant=\"destructive\"\n size=\"sm\"\n disabled={disabled}\n onClick={() => setConfirming([open.id])}\n >\n {t('delete')}\n </Button>\n </div>\n )}\n </aside>\n )}\n </div>\n\n <Dialog open={confirming !== null} onOpenChange={(next) => !next && setConfirming(null)}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{t('confirmDeleteTitle', { count: confirming?.length ?? 0 })}</DialogTitle>\n </DialogHeader>\n <DialogDescription>{t('confirmDeleteBody')}</DialogDescription>\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"outline\" onClick={() => setConfirming(null)}>\n {t('cancel')}\n </Button>\n <Button variant=\"destructive\" disabled={busy} onClick={() => void confirmDelete()}>\n {t('confirmDelete')}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n\n/**\n * \"Export CSV\" — a plain download link to the export of the filters on screen.\n * A link cannot carry the site header the admin's own requests send, so the\n * selected site is added as `_site` when it is clicked, from the page URL.\n */\nexport function SubmissionsExportLink({ href }: { readonly href: string }): ReactElement {\n const t = useTranslations('Forms.submissions')\n return (\n <Button variant=\"outline\" size=\"sm\" asChild>\n <a\n href={href}\n download\n // Before the link can be followed by any means — click, middle-click,\n // the context menu's \"Save link as\", Enter.\n onPointerDown={(event) => {\n event.currentTarget.href = withAdminSiteParam(href)\n }}\n onFocus={(event) => {\n event.currentTarget.href = withAdminSiteParam(href)\n }}\n onClick={(event) => {\n event.currentTarget.href = withAdminSiteParam(href)\n }}\n >\n {t('exportCsv')}\n </a>\n </Button>\n )\n}\n"],"mappings":"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"}
|
package/dist/pages.d.mts
CHANGED
|
@@ -17,5 +17,21 @@ declare function FormsListPage({
|
|
|
17
17
|
locale: string;
|
|
18
18
|
}>): Promise<ReactNode>;
|
|
19
19
|
//#endregion
|
|
20
|
-
|
|
20
|
+
//#region src/pages/privacy-page.d.ts
|
|
21
|
+
declare function PrivacyRequestsPage({
|
|
22
|
+
params
|
|
23
|
+
}: PageProps<{
|
|
24
|
+
locale: string;
|
|
25
|
+
}>): Promise<ReactNode>;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/pages/submissions-page.d.ts
|
|
28
|
+
declare function SubmissionsPage({
|
|
29
|
+
params,
|
|
30
|
+
searchParams
|
|
31
|
+
}: PageProps<{
|
|
32
|
+
locale: string;
|
|
33
|
+
id: string;
|
|
34
|
+
}>): Promise<ReactNode>;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { FormCanvasPage, FormsListPage, PrivacyRequestsPage, SubmissionsPage };
|
|
21
37
|
//# sourceMappingURL=pages.d.mts.map
|
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","../src/pages/privacy-page.tsx","../src/pages/submissions-page.tsx"],"mappings":";;;;iBA4BsB,cAAA,CAAA;EACpB,MAAA;EACA;AAAA,GACC,SAAA;EAAY,MAAA;EAAgB,EAAA;AAAA,KAAgB,OAAA,CAAQ,SAAA;;;iBCgCjC,aAAA,CAAA;EAAgB;AAAA,GAAU,SAAA;EAAY,MAAA;AAAA,KAAoB,OAAA,CAAQ,SAAA;;;iBCnClE,mBAAA,CAAA;EACpB;AAAA,GACC,SAAA;EAAY,MAAA;AAAA,KAAoB,OAAA,CAAQ,SAAA;;;iBCuBrB,eAAA,CAAA;EACpB,MAAA;EACA;AAAA,GACC,SAAA;EAAY,MAAA;EAAgB,EAAA;AAAA,KAAgB,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{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};
|
|
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{getApp as i,getContext as a,isTokenDerivedDoor as o,requireRequestSite as s}from"@murumets-ee/core";import{FORMS_AUTO_REPLY_TYPE as c,FORMS_NOTIFICATION_TYPE as l,FORMS_RESOURCE as u,FORMS_SETTINGS_NAMESPACE as d,FORM_ENTITY as f,FORM_SUBMISSIONS_RESOURCE as p,Form as m,STATUS_FILTERS as h,SUBMISSIONS_PAGE_SIZE as g,collectRetiredColumns as _,compileForm as v,currentFormFields as y,ensureFormEmailDefaults as b,formSpamCounts as x,formsSettings as S,lastRetentionRun as C,listSubmissions as w,parseSubmissionFilters as T,readDeliveries as ee,readEmailSettings as E,submissionColumns as te,submissionCounts as D,submissionFilterParams as ne,submissionPages as re}from"@murumets-ee/forms";import{FormCanvasFactsProvider as O}from"@murumets-ee/forms/editor";import{notFound as k,redirect as A}from"next/navigation";import{challengeSecretFor as j}from"@murumets-ee/forms/server";import{instanceTypeId as M}from"@murumets-ee/notifications/define";import{SettingsClient as N}from"@murumets-ee/settings";import{jsx as P,jsxs as F}from"react/jsx-runtime";import{fetchEntityList as I}from"@murumets-ee/admin-ui/server";import{WorkflowStatusBadge as L}from"@murumets-ee/admin-ui/workflow";import{countEmbedUsages as R}from"@murumets-ee/blocks/server";import{DeleteFormButton as z,NewFormDialog as B,PrivacyRequestsView as V,SubmissionsExportLink as ie,SubmissionsView as ae}from"@murumets-ee/forms-ui";import{Button as H,Table as U,TableBody as W,TableCell as G,TableHead as K,TableHeader as q,TableRow as J}from"@murumets-ee/ui";import Y from"next/link";import{getFormatter as oe,getTimeZone as se,getTranslations as X,setRequestLocale as Z}from"next-intl/server";async function Q(e){let t=new Map;if(e.length===0)return t;let n=await x.makeClient(i().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 ce(){let e=i(),t=await s(d,e),n=new N(S,{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)&&j(`turnstile`,t,process.env)!==void 0,recaptcha:o(a)&&j(`recaptcha`,t,process.env)!==void 0}}async function le(e){let t=i(),n=await t.getSystemClient(m).findById(e.formId);if(n==null)return null;await b(t,e.formId);let r=await E(t,e.formId),a=(v(n.body)?.fields??[]).filter(e=>e.kind===`email`).map(e=>({key:e.key,label:e.label===``?e.key:e.label})),o=t=>e.canEditTemplates?`${e.adminBase}/notifications/templates/${encodeURIComponent(M(t,e.formId))}/email`:null;return{formId:e.formId,recipients:r.recipients,autoReplyEnabled:r.autoReplyEnabled,autoReplyField:r.autoReplyField,emailFields:a,notificationHref:o(l),autoReplyHref:o(c),apiBasePath:e.apiBasePath}}const ue=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function de({params:t,searchParams:a}){let{id:o,locale:s}=await t;ue.test(o)||k();let c=e(),l=await r(m,c,{params:t,searchParams:a}),u=await c.withAdminContext(s,async()=>{let e=c.adminBasePath.replace(`:locale`,s),[t,r,a,l]=await Promise.all([Q([o]).catch(()=>(console.warn(`forms: the blocked count for form ${o} could not be read`),null)),ce(),n(p,`view`,c).then(async e=>e?(await D(i(),[o])).get(o)?.fresh??0:null).catch(()=>(console.warn(`forms: the new-submissions count for form ${o} could not be read`),null)),n(`message_template`,`update`,c).then(t=>le({formId:o,adminBase:e,apiBasePath:c.apiBasePath??`/api/admin`,canEditTemplates:t})).catch(()=>(console.warn(`forms: the email settings of form ${o} could not be read`),null))]);return{discarded:t===null?null:t.get(o)??0,ready:r,settingsHref:`${e}/settings/${d}`,...a!==null&&{submissions:{href:`${e}/forms/${o}/submissions`,fresh:a}},...l!==null&&{emails:l}}}).catch(()=>(console.warn(`forms: the spam facts for form ${o} could not be read`),null));return u===null?l:P(O,{facts:u,children:l})}async function fe({params:r}){let a=e(),{locale:o}=await r;Z(o);let s=await X(`Forms.list`),c=`${a.adminBasePath.replace(`:locale`,o)}/forms`;return a.withAdminContext(o,async()=>{await t(m,`view`,a)||k();let[{items:e,total:r},o,l,d,h]=await Promise.all([I(m,{sortField:`name`,sortDirection:`asc`,limit:200}),n(`form`,`create`,a),n(`form`,`delete`,a),n(u,`manage`,a),n(p,`view`,a)]),g=a.apiBasePath??`/api/admin`,_=await R(f,e.map(e=>e.id)).catch(()=>null),v=d||h?await Q(e.map(e=>String(e.id))).catch(()=>(console.warn(`forms: the blocked counts for the Forms list could not be read`),null)):null,y=h?await D(i(),e.map(e=>String(e.id))).catch(()=>(console.warn(`forms: the submission counts for the Forms list could not be read`),null)):null,b=e=>`${c}/${String(e)}/submissions`,x=h?await C(i()).catch(()=>{console.warn(`forms: the last clean-up run for the Forms list could not be read`)}):void 0,S=await oe();return F(`div`,{className:`space-y-4 p-6`,children:[F(`div`,{className:`flex items-start justify-between gap-4`,children:[F(`div`,{children:[P(`h1`,{className:`text-2xl font-bold`,children:s(`title`)}),P(`p`,{className:`text-sm text-muted-foreground`,children:s(`description`)})]}),F(`div`,{className:`flex items-center gap-2`,children:[h&&P(H,{variant:`outline`,size:`sm`,asChild:!0,children:P(Y,{href:`${c}/privacy`,"data-forms-privacy-link":``,children:s(`privacy`)})}),o&&d&&P(B,{apiBasePath:g,formsBasePath:c})]})]}),e.length===0?P(`p`,{className:`rounded-md border border-dashed p-6 text-sm text-muted-foreground`,children:s(`empty`)}):F(U,{children:[P(q,{children:F(J,{children:[P(K,{children:s(`name`)}),P(K,{children:s(`usedOn`)}),P(K,{className:`text-right`,children:s(`submissions`)}),P(K,{className:`text-right`,children:s(`new`)}),P(K,{className:`text-right`,children:s(`blocked`)}),P(K,{children:s(`status`)}),l&&d&&P(K,{className:`w-0`})]})}),P(W,{children:e.map(e=>F(J,{children:[P(G,{className:`font-medium`,children:d||h?P(Y,{href:d?`${c}/${e.id}`:b(e.id),className:`text-primary hover:underline`,children:e.name}):e.name}),P(G,{className:`text-muted-foreground`,"data-forms-used-on":e.id,children:_===null?s(`notYet`):s(`usedOnPages`,{count:_.get(e.id)??0})}),P(G,{className:`text-right tabular-nums`,"data-forms-submissions":e.id,children:y===null?P(`span`,{className:`text-muted-foreground`,children:s(`notYet`)}):P(Y,{href:b(e.id),className:`text-primary hover:underline`,"aria-label":s(`submissionsOf`,{form:String(e.name),count:y.get(String(e.id))?.total??0}),children:y.get(String(e.id))?.total??0})}),P(G,{className:`text-right tabular-nums`,"data-forms-new":e.id,children:y===null?P(`span`,{className:`text-muted-foreground`,children:s(`notYet`)}):y.get(String(e.id))?.fresh??0}),P(G,{className:`text-right tabular-nums`,children:v===null?s(`notYet`):v.get(String(e.id))??0}),P(G,{children:P(L,{status:e.status===`published`?`published`:`draft`})}),l&&d&&P(G,{className:`text-right`,children:P(z,{apiBasePath:g,formId:String(e.id),formName:String(e.name),submissions:y===null?null:y.get(String(e.id))?.total??0,pages:_===null?null:_.get(e.id)??0})})]},e.id))})]}),r>e.length&&P(`p`,{className:`text-xs text-muted-foreground`,children:s(`truncated`,{count:e.length})}),x!==void 0&&P(`p`,{className:`text-xs text-muted-foreground`,"data-forms-retention-run":``,children:x===null?s(`retentionNever`):`${s(`retentionRun`,{when:S.dateTime(new Date(x.lastRunAt),{dateStyle:`medium`,timeStyle:`short`}),count:x.removed})}${x.unfinished?` ${s(`retentionUnfinished`)}`:``}`})]})})}async function pe({params:t}){let r=e(),{locale:i}=await t;Z(i);let s=await X(`Forms.privacy`),c=`${r.adminBasePath.replace(`:locale`,i)}/forms`;return r.withAdminContext(i,async()=>{await n(p,`view`,r)||k();let e=!o(a()?.door)&&await n(p,`manage`,r);return F(`div`,{className:`space-y-4 p-6`,children:[F(`div`,{children:[P(`p`,{className:`text-sm text-muted-foreground`,children:F(Y,{href:c,className:`hover:underline`,children:[`← `,s(`backToForms`)]})}),P(`h1`,{className:`text-2xl font-bold`,children:s(`title`)}),P(`p`,{className:`max-w-prose text-sm text-muted-foreground`,children:s(`intro`)})]}),P(V,{apiBasePath:r.apiBasePath??`/api/admin`,formsBasePath:c,locale:i,canErase:e})]})})}const me=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,$=e=>Array.isArray(e)?e[0]:e;async function he({params:t,searchParams:r}){let{id:a,locale:o}=await t;me.test(a)||k();let s=await r,c=e();Z(o);let l=await X(`Forms.submissions`),d=`${c.adminBasePath.replace(`:locale`,o)}/forms`,f=c.apiBasePath??`/api/admin`;return c.withAdminContext(o,async()=>{await n(p,`view`,c)||k();let e=i(),t=await e.getSystemClient(m).findById(a);t??k();let r=T(e=>$(s[e]),{timeZone:await se()}),v=Number($(s.p)),b=Number.isFinite(v)?Math.min(Math.max(Math.trunc(v),1),1e5):1,[{items:x,total:S},C,E,D,O]=await Promise.all([w(e,a,r,{limit:g,offset:(b-1)*g}),re(e,a),n(p,`manage`,c),n(u,`manage`,c),Q([a]).catch(()=>(console.warn(`forms: the blocked count for form ${a} could not be read`),null))]),j=await ee(e,a,x.map(e=>e.id)).then(e=>Object.fromEntries(e)).catch(()=>(console.warn(`forms: the email state of form ${a}'s submissions could not be read`),{})),M=y(t.body),N=te(M,_(M,x)),I=ne(r),L=new URLSearchParams(I);L.set(`form`,a),L.set(`uiLocale`,o),L.set(`tz`,r.timeZone);let R=e=>{let t=new URLSearchParams(I);e>1&&t.set(`p`,String(e));let n=t.toString();return`${d}/${a}/submissions${n===``?``:`?${n}`}`},z=Math.max(Math.ceil(S/g),1);x.length===0&&S>0&&b>z&&A(R(z));let B=I.toString()!==``,V=typeof t.name==`string`?t.name:``,U=O===null?null:O.get(a)??0,W=r.page!==void 0&&!C.includes(r.page)?[r.page,...C]:C;return F(`div`,{className:`space-y-4 p-6`,children:[F(`div`,{className:`flex flex-wrap items-start justify-between gap-4`,children:[F(`div`,{children:[P(`p`,{className:`text-sm text-muted-foreground`,children:F(Y,{href:d,className:`hover:underline`,children:[`← `,l(`backToForms`)]})}),P(`h1`,{className:`text-2xl font-bold`,children:l(`title`,{form:V})})]}),F(`div`,{className:`flex items-center gap-2`,children:[D&&P(H,{variant:`outline`,size:`sm`,asChild:!0,children:P(Y,{href:`${d}/${a}`,children:l(`openForm`)})}),P(ie,{href:`${f}/form-submissions/export?${L.toString()}`})]})]}),F(`form`,{method:`get`,action:`${d}/${a}/submissions`,className:`flex flex-wrap items-end gap-3 rounded-md border p-3 text-sm`,children:[F(`label`,{className:`flex flex-col gap-1`,children:[P(`span`,{className:`text-xs text-muted-foreground`,children:l(`filters.from`)}),P(`input`,{type:`date`,name:`from`,defaultValue:r.from??``,className:`h-8 rounded-md border bg-background px-2`})]}),F(`label`,{className:`flex flex-col gap-1`,children:[P(`span`,{className:`text-xs text-muted-foreground`,children:l(`filters.to`)}),P(`input`,{type:`date`,name:`to`,defaultValue:r.to??``,className:`h-8 rounded-md border bg-background px-2`})]}),F(`label`,{className:`flex flex-col gap-1`,children:[P(`span`,{className:`text-xs text-muted-foreground`,children:l(`filters.page`)}),F(`select`,{name:`page`,defaultValue:r.page??``,className:`h-8 rounded-md border bg-background px-2`,children:[P(`option`,{value:``,children:l(`filters.allPages`)}),W.map(e=>P(`option`,{value:e,children:e},e))]})]}),F(`label`,{className:`flex flex-col gap-1`,children:[P(`span`,{className:`text-xs text-muted-foreground`,children:l(`filters.status`)}),P(`select`,{name:`status`,defaultValue:r.status,className:`h-8 rounded-md border bg-background px-2`,children:h.map(e=>P(`option`,{value:e,children:l(`filters.statuses.${e}`)},e))})]}),P(H,{type:`submit`,size:`sm`,variant:`secondary`,children:l(`filters.apply`)}),B&&P(H,{size:`sm`,variant:`ghost`,asChild:!0,children:P(Y,{href:`${d}/${a}/submissions`,children:l(`filters.clear`)})}),U!==null&&U>0&&P(`span`,{className:`ml-auto text-xs text-muted-foreground`,"data-forms-blocked":!0,children:l(`blocked`,{count:U})})]}),P(ae,{formId:a,rows:x,columns:N,canManage:E,deliveries:j,apiBasePath:f,emptyText:l(B?`emptyFiltered`:`empty`)},`${I.toString()}#${b}`),F(`div`,{className:`flex items-center justify-between text-sm text-muted-foreground`,children:[P(`span`,{children:l(`showing`,{shown:x.length,total:S})}),z>1&&F(`span`,{className:`flex items-center gap-2`,children:[b>1&&F(Y,{href:R(b-1),className:`hover:underline`,children:[`← `,l(`previous`)]}),P(`span`,{children:l(`pageOf`,{page:Math.min(b,z),pages:z})}),b<z&&F(Y,{href:R(b+1),className:`hover:underline`,children:[l(`next`),` →`]})]})]})]})})}export{de as FormCanvasPage,fe as FormsListPage,pe as PrivacyRequestsPage,he as SubmissionsPage};
|
|
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-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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"}
|
|
1
|
+
{"version":3,"file":"pages.mjs","names":["UUID_RE"],"sources":["../src/pages/form-facts.ts","../src/pages/form-canvas-page.tsx","../src/pages/forms-list-page.tsx","../src/pages/privacy-page.tsx","../src/pages/submissions-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 {\n compileForm,\n ensureFormEmailDefaults,\n FORMS_AUTO_REPLY_TYPE,\n FORMS_NOTIFICATION_TYPE,\n FORMS_SETTINGS_NAMESPACE,\n Form,\n formSpamCounts,\n formsSettings,\n readEmailSettings,\n} from '@murumets-ee/forms'\nimport type { FormEmailFacts } from '@murumets-ee/forms/editor'\nimport { challengeSecretFor } from '@murumets-ee/forms/server'\nimport { instanceTypeId } from '@murumets-ee/notifications/define'\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/**\n * The Emails tab's facts for form `formId` (forms D006): its recipients and\n * auto-reply, its email fields, and where its two emails are edited.\n *\n * A form made before emails existed has no defaults yet; they are added here —\n * idempotently, behind the page's `forms:manage` gate — so the editor link\n * always opens a real template.\n *\n * `canEditTemplates` is the viewer's own `message_template:update`: the\n * template editor is the administrator's, so an editor without it is told so\n * rather than shown a link that 404s.\n */\nexport async function emailFacts(args: {\n formId: string\n adminBase: string\n apiBasePath: string\n canEditTemplates: boolean\n}): Promise<FormEmailFacts | null> {\n const app = getApp()\n const row: Record<string, unknown> | null | undefined = await app\n .getSystemClient(Form)\n .findById(args.formId)\n if (row === null || row === undefined) return null\n await ensureFormEmailDefaults(app, args.formId)\n const settings = await readEmailSettings(app, args.formId)\n const emailFields = (compileForm(row.body)?.fields ?? [])\n .filter((field) => field.kind === 'email')\n .map((field) => ({ key: field.key, label: field.label !== '' ? field.label : field.key }))\n const editor = (base: string): string | null =>\n args.canEditTemplates\n ? `${args.adminBase}/notifications/templates/${encodeURIComponent(instanceTypeId(base, args.formId))}/email`\n : null\n return {\n formId: args.formId,\n recipients: settings.recipients,\n autoReplyEnabled: settings.autoReplyEnabled,\n autoReplyField: settings.autoReplyField,\n emailFields,\n notificationHref: editor(FORMS_NOTIFICATION_TYPE),\n autoReplyHref: editor(FORMS_AUTO_REPLY_TYPE),\n apiBasePath: args.apiBasePath,\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 { hasResourcePermission, renderEntityEditFallback } from '@murumets-ee/admin-ui/pages'\nimport { getApp, type PageProps } from '@murumets-ee/core'\nimport {\n FORM_SUBMISSIONS_RESOURCE,\n FORMS_SETTINGS_NAMESPACE,\n Form,\n submissionCounts,\n} from '@murumets-ee/forms'\nimport { FormCanvasFactsProvider } from '@murumets-ee/forms/editor'\nimport { notFound } from 'next/navigation'\nimport type { ReactNode } from 'react'\nimport { challengeReadiness, discardedCounts, emailFacts } 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 adminBase = config.adminBasePath.replace(':locale', locale)\n const [counts, ready, fresh, emails] = 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 // The way to this form's Submissions (W4), with how many are new —\n // only for a person who may open them, and never at the editor's cost.\n hasResourcePermission(FORM_SUBMISSIONS_RESOURCE, 'view', config)\n .then(async (allowed) =>\n allowed ? ((await submissionCounts(getApp(), [id])).get(id)?.fresh ?? 0) : null,\n )\n .catch(() => {\n console.warn(`forms: the new-submissions count for form ${id} could not be read`)\n return null\n }),\n // The Emails tab (D006). Without it the tab points at the Forms list.\n hasResourcePermission('message_template', 'update', config)\n .then((canEditTemplates) =>\n emailFacts({\n formId: id,\n adminBase,\n apiBasePath: config.apiBasePath ?? '/api/admin',\n canEditTemplates,\n }),\n )\n .catch(() => {\n console.warn(`forms: the email settings of form ${id} could not be read`)\n return null\n }),\n ])\n return {\n discarded: counts === null ? null : (counts.get(id) ?? 0),\n ready,\n settingsHref: `${adminBase}/settings/${FORMS_SETTINGS_NAMESPACE}`,\n ...(fresh !== null && {\n submissions: { href: `${adminBase}/forms/${id}/submissions`, fresh },\n }),\n ...(emails !== null && { emails }),\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 * and its submissions.\n *\n * Gated by `form:view` on its route (the plugin's `defaultRoutes`), which is\n * also what the sidebar link inherits: seeing the list is seeing this site's\n * forms. What a person may DO from it follows their job permissions, each\n * asked here and each only deciding what is drawn —\n *\n * - `forms:manage` (build forms): the name opens the canvas, and \"New form\".\n * - `form_submissions:view`: the Submissions and New counts, each a link to\n * the form's Submissions screen (W4). For a person who holds this and not\n * `forms:manage` — the read-only role of D005 — the name opens Submissions\n * too, so the list is their way in. Also the \"Privacy requests\" link (W5)\n * and the line saying when the daily clean-up last ran and what it removed\n * (D007).\n * - `form:delete` together with `forms:manage`: \"Delete…\" on a row — the form\n * AND its submissions, after a confirm that names the count (the route\n * decides; see `handleRemove`).\n *\n * Lists THIS site's forms only: `form` is site-bound, so the entity read is\n * scoped by the request's site like 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 (D004). A number that could not be read, or that this person\n * may not see, is a dash — never a zero that claims a count nobody took.\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 { getApp, type PageProps } from '@murumets-ee/core'\nimport {\n FORM_ENTITY,\n FORM_SUBMISSIONS_RESOURCE,\n FORMS_RESOURCE,\n Form,\n lastRetentionRun,\n submissionCounts,\n} from '@murumets-ee/forms'\nimport { DeleteFormButton, NewFormDialog } from '@murumets-ee/forms-ui'\nimport {\n Button,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@murumets-ee/ui'\nimport Link from 'next/link'\nimport { notFound } from 'next/navigation'\nimport { getFormatter, 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 `form:view`, asked again here because this is the\n // read it guards: a page rendered past its gate must still not list.\n if (!(await hasEntityPermission(Form, 'view', config))) notFound()\n const [{ items, total }, canCreate, canDelete, canBuild, canSeeSubmissions] = await Promise.all(\n [\n fetchEntityList(Form, { sortField: 'name', sortDirection: 'asc', limit: MAX_FORMS }),\n hasResourcePermission('form', 'create', config),\n hasResourcePermission('form', 'delete', config),\n hasResourcePermission(FORMS_RESOURCE, 'manage', config),\n hasResourcePermission(FORM_SUBMISSIONS_RESOURCE, 'view', config),\n ],\n )\n const apiBasePath = config.apiBasePath ?? '/api/admin'\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 // Shown to people who build forms or read submissions — not to a role that\n // can only see that the forms exist.\n const blocked = !(canBuild || canSeeSubmissions)\n ? null\n : 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 // Stored and new submissions per form — one grouped query for the list,\n // and only for a person who may see submissions at all.\n const received = canSeeSubmissions\n ? await submissionCounts(\n getApp(),\n items.map((form) => String(form.id)),\n ).catch(() => {\n console.warn('forms: the submission counts for the Forms list could not be read')\n return null\n })\n : null\n const submissionsHref = (id: unknown) => `${formsBasePath}/${String(id)}/submissions`\n // When the daily clean-up last ran here and what it removed (D007) — for\n // the people whose submissions it removes. `undefined`: could not be read\n // (the line is left out); `null`: it has never run on this site.\n const lastRun = canSeeSubmissions\n ? await lastRetentionRun(getApp()).catch(() => {\n console.warn('forms: the last clean-up run for the Forms list could not be read')\n return undefined\n })\n : undefined\n const format = await getFormatter()\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 <div className=\"flex items-center gap-2\">\n {canSeeSubmissions && (\n <Button variant=\"outline\" size=\"sm\" asChild>\n <Link href={`${formsBasePath}/privacy`} data-forms-privacy-link=\"\">\n {t('privacy')}\n </Link>\n </Button>\n )}\n {canCreate && canBuild && (\n <NewFormDialog apiBasePath={apiBasePath} formsBasePath={formsBasePath} />\n )}\n </div>\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 {canDelete && canBuild && <TableHead className=\"w-0\" />}\n </TableRow>\n </TableHeader>\n <TableBody>\n {items.map((form) => (\n <TableRow key={form.id}>\n <TableCell className=\"font-medium\">\n {canBuild || canSeeSubmissions ? (\n <Link\n href={canBuild ? `${formsBasePath}/${form.id}` : submissionsHref(form.id)}\n className=\"text-primary hover:underline\"\n >\n {form.name}\n </Link>\n ) : (\n form.name\n )}\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 tabular-nums\" data-forms-submissions={form.id}>\n {received === null ? (\n <span className=\"text-muted-foreground\">{t('notYet')}</span>\n ) : (\n <Link\n href={submissionsHref(form.id)}\n className=\"text-primary hover:underline\"\n aria-label={t('submissionsOf', {\n form: String(form.name),\n count: received.get(String(form.id))?.total ?? 0,\n })}\n >\n {received.get(String(form.id))?.total ?? 0}\n </Link>\n )}\n </TableCell>\n <TableCell className=\"text-right tabular-nums\" data-forms-new={form.id}>\n {received === null ? (\n <span className=\"text-muted-foreground\">{t('notYet')}</span>\n ) : (\n (received.get(String(form.id))?.fresh ?? 0)\n )}\n </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 {canDelete && canBuild && (\n <TableCell className=\"text-right\">\n <DeleteFormButton\n apiBasePath={apiBasePath}\n formId={String(form.id)}\n formName={String(form.name)}\n submissions={\n received === null ? null : (received.get(String(form.id))?.total ?? 0)\n }\n pages={usedOn === null ? null : (usedOn.get(form.id) ?? 0)}\n />\n </TableCell>\n )}\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 {lastRun !== undefined && (\n <p className=\"text-xs text-muted-foreground\" data-forms-retention-run=\"\">\n {lastRun === null\n ? t('retentionNever')\n : `${t('retentionRun', {\n when: format.dateTime(new Date(lastRun.lastRunAt), {\n dateStyle: 'medium',\n timeStyle: 'short',\n }),\n count: lastRun.removed,\n })}${lastRun.unfinished ? ` ${t('retentionUnfinished')}` : ''}`}\n </p>\n )}\n </div>\n )\n })\n}\n","/**\n * Privacy requests (W5) — find one person's submissions on this site by email\n * address, export them, erase them (forms D007).\n *\n * `/admin/forms/privacy`, reached from the Forms list's header; never a menu\n * entry of its own. Gated on `form_submissions:view` by its route (the\n * plugin's `defaultRoutes`, forms D011) — the same permission that opens a\n * form's Submissions, because what this screen reaches is the same answers.\n * Erasing additionally needs `form_submissions:manage` AND a person's own\n * session — the erase route is closed to every agent door (`privacy-routes.ts`\n * says why); without both the button is not drawn, and the route refuses in\n * any case.\n *\n * The page itself reads NOTHING about anyone: it draws the search box. The\n * address is typed into the client view and sent in a request body\n * (`privacy-routes.ts`), so it never appears in a URL.\n */\n\nimport { getRegisteredAdminConfig } from '@murumets-ee/admin-ui/config-registry'\nimport { hasResourcePermission } from '@murumets-ee/admin-ui/pages'\nimport { getContext, isTokenDerivedDoor, type PageProps } from '@murumets-ee/core'\nimport { FORM_SUBMISSIONS_RESOURCE } from '@murumets-ee/forms'\nimport { PrivacyRequestsView } from '@murumets-ee/forms-ui'\nimport Link from 'next/link'\nimport { notFound } from 'next/navigation'\nimport { getTranslations, setRequestLocale } from 'next-intl/server'\nimport type { ReactNode } from 'react'\n\nexport async function PrivacyRequestsPage({\n params,\n}: PageProps<{ locale: string }>): Promise<ReactNode> {\n const config = getRegisteredAdminConfig()\n const { locale } = await params\n setRequestLocale(locale)\n const t = await getTranslations('Forms.privacy')\n const formsBasePath = `${config.adminBasePath.replace(':locale', locale)}/forms`\n\n return config.withAdminContext(locale, async () => {\n // The route's gate, asked again: a page rendered past its gate must still\n // offer nothing.\n if (!(await hasResourcePermission(FORM_SUBMISSIONS_RESOURCE, 'view', config))) notFound()\n // An agent's browser session finds and exports; it is never offered an\n // erase its route would refuse.\n // The door is read from the request context the app's `withAdminContext`\n // establishes (the in-repo admin and the scaffold both carry it). An app\n // whose own copy does not would draw the button for an agent session —\n // and the route would still refuse it, with the reason shown.\n const canErase =\n !isTokenDerivedDoor(getContext()?.door) &&\n (await hasResourcePermission(FORM_SUBMISSIONS_RESOURCE, 'manage', config))\n return (\n <div className=\"space-y-4 p-6\">\n <div>\n <p className=\"text-sm text-muted-foreground\">\n <Link href={formsBasePath} className=\"hover:underline\">\n ← {t('backToForms')}\n </Link>\n </p>\n <h1 className=\"text-2xl font-bold\">{t('title')}</h1>\n <p className=\"max-w-prose text-sm text-muted-foreground\">{t('intro')}</p>\n </div>\n <PrivacyRequestsView\n apiBasePath={config.apiBasePath ?? '/api/admin'}\n formsBasePath={formsBasePath}\n locale={locale}\n canErase={canErase}\n />\n </div>\n )\n })\n}\n","/**\n * Submissions (W4) — what visitors sent through one form.\n *\n * `/admin/forms/:id/submissions`, gated on `form_submissions:view` by its\n * route (the plugin's `defaultRoutes`). The rows are read HERE, on the server,\n * through `@murumets-ee/forms`' submission functions — an elevated read the\n * page's gate has authorized, confined to the request's site (see\n * `submissions.ts`): another site's form id is a 404, and so is a form of\n * this site for a role without the permission.\n *\n * The filters are a plain GET form — status, page, day range — so the URL is\n * the state: it can be bookmarked, it works without JavaScript, and \"Export\n * CSV\" exports exactly what the URL names.\n *\n * Columns: one per field of the form as it is saved now, then one per retired\n * key that an answer on this page still holds (D005).\n */\n\nimport { getRegisteredAdminConfig } from '@murumets-ee/admin-ui/config-registry'\nimport { hasResourcePermission } from '@murumets-ee/admin-ui/pages'\nimport { getApp, type PageProps } from '@murumets-ee/core'\nimport {\n collectRetiredColumns,\n currentFormFields,\n FORM_SUBMISSIONS_RESOURCE,\n FORMS_RESOURCE,\n Form,\n listSubmissions,\n parseSubmissionFilters,\n readDeliveries,\n STATUS_FILTERS,\n SUBMISSIONS_PAGE_SIZE,\n submissionColumns,\n submissionFilterParams,\n submissionPages,\n} from '@murumets-ee/forms'\nimport { SubmissionsExportLink, SubmissionsView } from '@murumets-ee/forms-ui'\nimport { Button } from '@murumets-ee/ui'\nimport Link from 'next/link'\nimport { notFound, redirect } from 'next/navigation'\nimport { getTimeZone, getTranslations, setRequestLocale } from 'next-intl/server'\nimport type { ReactNode } from 'react'\nimport { discardedCounts } from './form-facts.js'\n\n/** Far past any real list; keeps a hand-typed `?p=` from becoming an absurd offset. */\nconst MAX_PAGE = 100_000\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\n/** The first value of a query parameter that may repeat. */\nconst first = (value: string | string[] | undefined): string | undefined =>\n Array.isArray(value) ? value[0] : value\n\nexport async function SubmissionsPage({\n params,\n searchParams,\n}: PageProps<{ locale: string; id: string }>): Promise<ReactNode> {\n const { id, locale } = await params\n if (!UUID_RE.test(id)) notFound()\n const query = await searchParams\n const config = getRegisteredAdminConfig()\n setRequestLocale(locale)\n const t = await getTranslations('Forms.submissions')\n const formsBasePath = `${config.adminBasePath.replace(':locale', locale)}/forms`\n const apiBasePath = config.apiBasePath ?? '/api/admin'\n\n return config.withAdminContext(locale, async () => {\n // The route's gate is `form_submissions:view`, asked again here because\n // everything below reads through an elevated client: a page rendered past\n // its gate must still show nothing.\n if (!(await hasResourcePermission(FORM_SUBMISSIONS_RESOURCE, 'view', config))) notFound()\n const app = getApp()\n // The form of THIS site, or nothing: the elevated read keeps the request's\n // site, so another site's id is not found.\n const form: Record<string, unknown> | null | undefined = await app\n .getSystemClient(Form)\n .findById(id)\n if (form === null || form === undefined) notFound()\n\n // Days are days of the zone the rows' times are shown in (next-intl's),\n // so \"from the 9th\" matches the clock a person reads beside each row.\n const timeZone = await getTimeZone()\n const filters = parseSubmissionFilters((name) => first(query[name]), { timeZone })\n // A whole page number within reach — `?p=1e400` is not one.\n const asked = Number(first(query.p))\n const pageNumber = Number.isFinite(asked)\n ? Math.min(Math.max(Math.trunc(asked), 1), MAX_PAGE)\n : 1\n const [{ items, total }, pages, canManage, canBuild, blocked] = await Promise.all([\n listSubmissions(app, id, filters, {\n limit: SUBMISSIONS_PAGE_SIZE,\n offset: (pageNumber - 1) * SUBMISSIONS_PAGE_SIZE,\n }),\n submissionPages(app, id),\n hasResourcePermission(FORM_SUBMISSIONS_RESOURCE, 'manage', config),\n hasResourcePermission(FORMS_RESOURCE, 'manage', config),\n // Supplementary: a failed read costs the line, never the screen.\n discardedCounts([id]).catch(() => {\n console.warn(`forms: the blocked count for form ${id} could not be read`)\n return null\n }),\n ])\n\n // Each shown submission's email state (D006) — supplementary: a failed read\n // costs the column, never the screen.\n const deliveries = await readDeliveries(\n app,\n id,\n items.map((item) => item.id),\n )\n .then((byId) => Object.fromEntries(byId))\n .catch(() => {\n console.warn(`forms: the email state of form ${id}'s submissions could not be read`)\n return {}\n })\n\n const current = currentFormFields(form.body)\n const columns = submissionColumns(current, collectRetiredColumns(current, items))\n const filterQuery = submissionFilterParams(filters)\n const exportQuery = new URLSearchParams(filterQuery)\n exportQuery.set('form', id)\n exportQuery.set('uiLocale', locale)\n exportQuery.set('tz', filters.timeZone)\n const pageHref = (page: number): string => {\n const next = new URLSearchParams(filterQuery)\n if (page > 1) next.set('p', String(page))\n const text = next.toString()\n return `${formsBasePath}/${id}/submissions${text === '' ? '' : `?${text}`}`\n }\n const lastPage = Math.max(Math.ceil(total / SUBMISSIONS_PAGE_SIZE), 1)\n // Past the end — the last rows of the page were deleted or archived, or\n // the URL was typed: show the last page that has rows, never an empty one\n // that reads as \"no submissions\".\n if (items.length === 0 && total > 0 && pageNumber > lastPage) redirect(pageHref(lastPage))\n const filtered = filterQuery.toString() !== ''\n const name = typeof form.name === 'string' ? form.name : ''\n const discarded = blocked === null ? null : (blocked.get(id) ?? 0)\n // The page filter keeps a value that is no longer among the options (its\n // last submission was deleted), so the select never shows a blank.\n const pageOptions =\n filters.page !== undefined && !pages.includes(filters.page) ? [filters.page, ...pages] : pages\n\n return (\n <div className=\"space-y-4 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-4\">\n <div>\n <p className=\"text-sm text-muted-foreground\">\n <Link href={formsBasePath} className=\"hover:underline\">\n ← {t('backToForms')}\n </Link>\n </p>\n <h1 className=\"text-2xl font-bold\">{t('title', { form: name })}</h1>\n </div>\n <div className=\"flex items-center gap-2\">\n {canBuild && (\n <Button variant=\"outline\" size=\"sm\" asChild>\n <Link href={`${formsBasePath}/${id}`}>{t('openForm')}</Link>\n </Button>\n )}\n <SubmissionsExportLink\n href={`${apiBasePath}/form-submissions/export?${exportQuery.toString()}`}\n />\n </div>\n </div>\n\n <form\n method=\"get\"\n action={`${formsBasePath}/${id}/submissions`}\n className=\"flex flex-wrap items-end gap-3 rounded-md border p-3 text-sm\"\n >\n <label className=\"flex flex-col gap-1\">\n <span className=\"text-xs text-muted-foreground\">{t('filters.from')}</span>\n <input\n type=\"date\"\n name=\"from\"\n defaultValue={filters.from ?? ''}\n className=\"h-8 rounded-md border bg-background px-2\"\n />\n </label>\n <label className=\"flex flex-col gap-1\">\n <span className=\"text-xs text-muted-foreground\">{t('filters.to')}</span>\n <input\n type=\"date\"\n name=\"to\"\n defaultValue={filters.to ?? ''}\n className=\"h-8 rounded-md border bg-background px-2\"\n />\n </label>\n <label className=\"flex flex-col gap-1\">\n <span className=\"text-xs text-muted-foreground\">{t('filters.page')}</span>\n <select\n name=\"page\"\n defaultValue={filters.page ?? ''}\n className=\"h-8 rounded-md border bg-background px-2\"\n >\n <option value=\"\">{t('filters.allPages')}</option>\n {pageOptions.map((path) => (\n <option key={path} value={path}>\n {path}\n </option>\n ))}\n </select>\n </label>\n <label className=\"flex flex-col gap-1\">\n <span className=\"text-xs text-muted-foreground\">{t('filters.status')}</span>\n <select\n name=\"status\"\n defaultValue={filters.status}\n className=\"h-8 rounded-md border bg-background px-2\"\n >\n {STATUS_FILTERS.map((status) => (\n <option key={status} value={status}>\n {t(`filters.statuses.${status}`)}\n </option>\n ))}\n </select>\n </label>\n <Button type=\"submit\" size=\"sm\" variant=\"secondary\">\n {t('filters.apply')}\n </Button>\n {filtered && (\n <Button size=\"sm\" variant=\"ghost\" asChild>\n <Link href={`${formsBasePath}/${id}/submissions`}>{t('filters.clear')}</Link>\n </Button>\n )}\n {discarded !== null && discarded > 0 && (\n <span className=\"ml-auto text-xs text-muted-foreground\" data-forms-blocked>\n {t('blocked', { count: discarded })}\n </span>\n )}\n </form>\n\n {/* Always mounted: the panel holds the submission being read, which may\n be the last one a filter listed. Keyed by what is listed, so moving\n to another page or filter starts with nothing open. */}\n <SubmissionsView\n key={`${filterQuery.toString()}#${pageNumber}`}\n formId={id}\n rows={items}\n columns={columns}\n canManage={canManage}\n deliveries={deliveries}\n apiBasePath={apiBasePath}\n emptyText={filtered ? t('emptyFiltered') : t('empty')}\n />\n\n <div className=\"flex items-center justify-between text-sm text-muted-foreground\">\n <span>{t('showing', { shown: items.length, total })}</span>\n {lastPage > 1 && (\n <span className=\"flex items-center gap-2\">\n {pageNumber > 1 && (\n <Link href={pageHref(pageNumber - 1)} className=\"hover:underline\">\n ← {t('previous')}\n </Link>\n )}\n <span>{t('pageOf', { page: Math.min(pageNumber, lastPage), pages: lastPage })}</span>\n {pageNumber < lastPage && (\n <Link href={pageHref(pageNumber + 1)} className=\"hover:underline\">\n {t('next')} →\n </Link>\n )}\n </span>\n )}\n </div>\n </div>\n )\n })\n}\n"],"mappings":"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"}
|
package/dist/plugin.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PluginNotificationTypeDefinition } from "@murumets-ee/core";
|
|
1
2
|
import { getFormsMessages } from "@murumets-ee/forms/i18n";
|
|
2
3
|
|
|
3
4
|
//#region ../admin-route/dist/index.d.mts
|
|
@@ -172,10 +173,15 @@ interface DefineFeatureSpec {
|
|
|
172
173
|
*/
|
|
173
174
|
//#endregion
|
|
174
175
|
//#region src/plugin.d.ts
|
|
176
|
+
/** The daily clean-up of expired submissions (forms D007) — see `runFormsRetention`. */
|
|
177
|
+
declare const FORMS_RETENTION_JOB = "forms:retention";
|
|
178
|
+
/** Nightly, off the hour so it does not start with every other 03:00 job. */
|
|
179
|
+
declare const FORMS_RETENTION_SCHEDULE = "23 3 * * *";
|
|
175
180
|
declare function formsPlugin(): Pick<import("@murumets-ee/core").Plugin, "server" | "adminUi" | "postInstall" | "requires" | "contributes"> & {
|
|
176
181
|
readonly name: "@murumets-ee/forms";
|
|
177
182
|
readonly shared?: {
|
|
178
183
|
readonly features: readonly DefineFeatureSpec[];
|
|
184
|
+
readonly notifications: PluginNotificationTypeDefinition[];
|
|
179
185
|
readonly settings: readonly [import("@murumets-ee/settings").SettingsDefinition<{
|
|
180
186
|
readonly turnstileSiteKey: import("@murumets-ee/settings").TextSettingConfig & {
|
|
181
187
|
readonly default: "";
|
|
@@ -196,5 +202,5 @@ declare function formsPlugin(): Pick<import("@murumets-ee/core").Plugin, "server
|
|
|
196
202
|
};
|
|
197
203
|
};
|
|
198
204
|
//#endregion
|
|
199
|
-
export { formsPlugin };
|
|
205
|
+
export { FORMS_RETENTION_JOB, FORMS_RETENTION_SCHEDULE, formsPlugin };
|
|
200
206
|
//# sourceMappingURL=plugin.d.mts.map
|
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","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":"
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":["PermissionString","AdminRouteMethod","SegmentPath","P","DefineAdminRouteSpec","TApp","AdminRouteHandler","RouteTokenChoice","AdminRouteMcpSpec","prefix","path","method","permission","defaultRoles","description","descriptionKey","handler","matchAnyPath","tokens","mcp","confirm","JsonValue","key","McpInputSchema","type","properties","required","additionalProperties","inputSchema","ENTRY_GATED","AdminRouteEntry","RouteTokenRefusalReason","resource","action","guardedHandler","acceptsTokens","tokenRefusal","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","ROUTE_TOKEN_REFUSAL_REASONS","refuse","RouteTokenDecision","refusal","matchTokenPolicyEntry","Pick","route","routeRequiresConfirmation","routeAcceptsTokens","AdminRouteTokenDecision","decision","RouteTokenDecisionSource","listAdminRouteTokenDecisions","routes","findUndecidedAdminRoutes","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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmwBU8D,iBAAAA;;;;;;EAMR9B,QAAAA;;;;;;;;EAQA+B,OAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0CAlD,YAAAA;;;;;;;EAOAC,WAAAA;AAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cC7uBW,mBAAA;;cAGA,wBAAA;AAAA,iBAEG,WAAA,CAAA,GAAW,IAAA,6BAAA,MAAA;EAAA;;gCAAA,iBAAA;IAAA,wBAiG4B,gCAAA;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,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};
|
|
1
|
+
import{combineAdminRoutes as e,defineAdminRoute as t,definePlugin as n,requireRequestSite as r}from"@murumets-ee/core";import{FORMS_MANAGE as i,FORMS_SETTINGS_NAMESPACE as a,FORM_SUBMISSIONS_MANAGE as o,FORM_SUBMISSIONS_VIEW as s,FORM_SUBMISSION_ENTITY as c,Form as l,FormSubmission as u,MAX_SUBMISSION_BULK as d,PRIVACY_EXPORT_MAX as f,PRIVACY_LIST_MAX as p,SUBMISSIONS_READ_MAX as m,collectRetiredColumns as h,compileForm as g,countSubmissionsByEmail as ee,countUnindexedSubmissions as _,currentFormFields as v,deleteSubmissions as te,enqueueFormEmail as ne,eraseSubmissionsByEmail as re,findSubmissionsByEmail as y,formEmailTypes as ie,formNames as b,formsFeatures as x,formsRoutes as S,formsSettings as ae,formsTables as oe,isEmailKind as se,isSubmissionStatus as ce,listSubmissionRows as le,parsePrivacyEmail as ue,parseRecipients as de,parseSubmissionFilters as fe,readCappedBody as C,readDeliveries as pe,setSubmissionStatus as me,submissionColumns as he,submissionCounts as ge,submissionFilterParams as _e,writeEmailSettings as ve}from"@murumets-ee/forms";import{getFormsMessages as w}from"@murumets-ee/forms/i18n";import{FormEmbedBlock as ye,formEnvironment as be,formPageEnvironmentExtension as xe,formSchemaBlocks as Se}from"@murumets-ee/forms/schema";import{FormSubmissionsHeaderAction as Ce}from"@murumets-ee/forms-ui/canvas-header-action";import{csvDelimiterFor as T,csvFilename as E,pagedExportRows as D,runCsvExport as O}from"@murumets-ee/admin-ui/csv-export";const k=`form-emails`,A=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function j(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`,"Cache-Control":`no-store`}})}function we(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}async function M(e){let t=await C(e,8192);if(`tooLarge`in t)return{refusal:j({error:`body_too_large`},413)};try{return{value:JSON.parse(t.text)}}catch{return{refusal:j({error:`invalid_json`},400)}}}function N(e,t){if(!we(e))return null;let n=new Set(t);return Object.keys(e).every(e=>n.has(e))?e:null}async function P(e,t){let n=await e.getSystemClient(l).findById(t);return n==null?null:{body:n.body}}const Te=async(e,t)=>{let n=await M(e);if(`refusal`in n)return n.refusal;let r=N(n.value,[`form`,`recipients`,`autoReplyEnabled`,`autoReplyField`]),{form:i,recipients:a,autoReplyEnabled:o,autoReplyField:s}=r??{};if(r===null||typeof i!=`string`||!A.test(i)||typeof o!=`boolean`||!(s===null||typeof s==`string`))return j({error:`Expected { form: uuid, recipients: string[], autoReplyEnabled: boolean, autoReplyField: string|null }`},400);let c=de(a);if(!c.ok)return j({error:`invalid_recipients`,invalid:c.invalid,tooMany:c.tooMany},400);let l=await P(t.app,i);if(l===null)return j({error:`not_found`},404);let u=(g(l.body)?.fields??[]).filter(e=>e.kind===`email`).map(e=>e.key);return s!==null&&!u.includes(s)?j({error:`invalid_auto_reply_field`},400):o&&s===null?j({error:`auto_reply_needs_field`},400):(await ve(t.app,i,{recipients:c.recipients,autoReplyEnabled:o,autoReplyField:s}),t.audit?.({action:`forms.emails.update`,entityType:`form`,entityId:i,metadata:{recipients:c.recipients.length,autoReply:o}}),j({recipients:c.recipients,autoReplyEnabled:o,autoReplyField:s}))},Ee=[`no_recipients`,`disabled`];function De(e,t){return e.status===`failed`?!(e.kind===`autoReply`&&e.errorClass===`timeout`&&t-new Date(e.updatedAt).getTime()<36e5):e.status===`queued`?t-new Date(e.updatedAt).getTime()>3e5:e.status===`skipped`&&e.errorClass!==null&&Ee.includes(e.errorClass)}const Oe=async(e,t)=>{let n=await M(e);if(`refusal`in n)return n.refusal;let i=N(n.value,[`form`,`submission`,`kind`]),{form:o,submission:s,kind:l}=i??{};if(i===null||typeof o!=`string`||!A.test(o)||typeof s!=`string`||!A.test(s)||!se(l))return j({error:`Expected { form: uuid, submission: uuid, kind: notification|autoReply }`},400);if(await P(t.app,o)===null)return j({error:`not_found`},404);let d=await t.app.getSystemClient(u).findById(s);if(d==null||String(d.form)!==o)return j({error:`not_found`},404);let f=((await pe(t.app,o,[s])).get(s)??[]).find(e=>e.kind===l);if(f!==void 0&&!De(f,Date.now()))return j({error:`not_retryable`,status:f.status},409);let p=await r(a,t.app),m=await ne(t.app,{site:p,formId:o,submissionId:s,kind:l});return t.audit?.({action:`forms.emails.retry`,entityType:c,entityId:s,metadata:{kind:l,queued:m}}),j({queued:m})},ke=t({prefix:k,path:``,method:`PATCH`,permission:i,defaultRoles:[`admin`,`editor`],description:`Set a form’s email recipients and auto-reply: { form, recipients, … }.`,descriptionKey:`Forms.permissions.routeEmails`,tokens:`accept`,confirm:!0,handler:Te}),Ae=t({prefix:k,path:`retry`,method:`POST`,permission:o,defaultRoles:[`admin`,`editor`],description:`Queue a failed form email again: { form, submission, kind }.`,descriptionKey:`Forms.permissions.routeEmailRetry`,tokens:`accept`,handler:Oe});function je(){return e([ke,Ae])}const F=`form-privacy`,I=`X-Lumi-Export-Truncated`;let L=0;function Me(){return new Response(JSON.stringify({error:`export_busy`,code:`export_busy`}),{status:503,headers:{"Content-Type":`application/json`,"Cache-Control":`no-store`,"Retry-After":`10`}})}function R(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`,"Cache-Control":`no-store`}})}function z(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}async function B(e,t){let n=await C(e,2048);if(`tooLarge`in n)return{refusal:R({error:`body_too_large`},413)};let r;try{r=JSON.parse(n.text)}catch{return{refusal:R({error:`invalid_json`},400)}}let i=new Set([`email`,...t]);if(!z(r)||Object.keys(r).some(e=>!i.has(e)))return{refusal:R({error:`invalid_body`},400)};let a=ue(r.email);return a===null?{refusal:R({error:`invalid_email`,code:`invalid_email`},400)}:{email:a,rest:r}}const Ne=async(e,t)=>{let n=await B(e,[]);if(`refusal`in n)return n.refusal;let{email:r}=n,[i,a,o]=await Promise.all([ee(t.app,r),y(t.app,r,{limit:p,offset:0}),_(t.app)]),s=await b(t.app,a.map(e=>e.formId)),c=a.map(e=>({id:e.id,formId:e.formId,formName:s.get(e.formId)??null,createdAt:e.createdAt,pagePath:e.pagePath}));return t.audit?.({action:`forms.privacy.search`,entityType:`form_submission`,metadata:{count:i,forms:[...new Set(a.map(e=>e.formId))],unindexed:o}}),R({total:i,items:c,unindexed:o,exportMax:f})},Pe=[`csv`,`json`];function Fe(e,t){let n=e.Forms;for(let e of t){if(!z(n))return;n=n[e]}return typeof n==`string`&&n!==``?n:void 0}async function Ie(e){let t=await w(e??`en`),n=(e,n)=>Fe(t,e)??n;return{form:n([`privacy`,`csv`,`form`],`Form`),received:n([`privacy`,`csv`,`received`],`Received (UTC)`),page:n([`privacy`,`csv`,`page`],`Page`),status:n([`privacy`,`csv`,`status`],`Status`),yes:n([`submissions`,`yes`],`yes`),no:n([`submissions`,`no`],`no`),deletedForm:n([`privacy`,`deletedForm`],`deleted`),statuses:{new:n([`submissionStatus`,`new`],`New`),read:n([`submissionStatus`,`read`],`Read`),archived:n([`submissionStatus`,`archived`],`Archived`)}}}function V(e,t){let n=e.fields[t]?.label;return n!==void 0&&n!==``?n:t}function Le(e){let t=new Map;for(let[n,r]of Object.entries(e.data)){let i=V(e,n);t.set(i,[...t.get(i)??[],r])}return t}function Re(e,t){if(e===void 0||e.length===0)return``;let n=e.map(e=>typeof e==`boolean`?e?t.yes:t.no:e),r=n[0];return n.length===1&&r!==void 0?r:n.join(` | `)}function ze(e){return e.slice(0,16).replace(`T`,` `)}function Be(e,t,n,r){return D((r,i)=>y(e,t,{limit:i,offset:r,until:n}),{pageSize:m,maxRows:f,...r!==void 0&&{onTruncated:r}})}const Ve=async(e,t)=>{let n=await B(e,[`format`,`uiLocale`]);if(`refusal`in n)return n.refusal;let{email:r,rest:i}=n,a=Pe.find(e=>e===i.format);if(a===void 0)return R({error:`invalid_format`},400);let o=typeof i.uiLocale==`string`&&i.uiLocale.length<=35?i.uiLocale:t.locale,s=t.app;if(L>=2)return Me();L+=1;let c=new Date,l=[],u=!1,d;try{for await(let e of Be(s,r,c,()=>{u=!0}))l.push(e);d=await b(s,[...new Set(l.map(e=>e.formId))])}finally{--L}u&&s.logger.warn({written:l.length},`forms: a privacy export reached the row cap — the file holds the newest rows only`);let f=()=>{t.audit?.({action:`forms.privacy.export`,entityType:`form_submission`,metadata:{count:l.length,forms:[...new Set(l.map(e=>e.formId))],format:a,truncated:u}})};if(a===`json`){let e=JSON.stringify({exportedAt:c.toISOString(),truncated:u,submissions:l.map(e=>({form:d.get(e.formId)??null,receivedAt:e.createdAt,page:e.pagePath,language:e.locale,status:e.status,answers:Object.entries(e.data).map(([t,n])=>({field:V(e,t),key:t,answer:n})),consent:Object.values(e.consent)}))},null,2);return f(),new Response(e,{status:200,headers:{"Content-Type":`application/json; charset=utf-8`,"Content-Disposition":`attachment; filename="${E(`privacy-request`).replace(/\.csv$/,`.json`)}"`,"Cache-Control":`private, no-store`,"X-Content-Type-Options":`nosniff`,[I]:String(u)}})}let p=await Ie(o),m=[],h=new Set;for(let e of l)for(let t of Object.keys(e.data)){let n=V(e,t);h.has(n)||(h.add(n),m.push(n))}let g=await O(()=>({filename:E(`privacy-request`),delimiter:T(o),header:[p.form,p.received,p.page,p.status,...m],rows:l.map(e=>{let t=Le(e);return[d.get(e.formId)??p.deletedForm,ze(e.createdAt),e.pagePath??``,p.statuses[e.status]??e.status,...m.map(e=>Re(t.get(e),p))]})}));return g.ok&&(f(),g.headers.set(I,String(u))),g},He=async(e,t)=>{let n=await B(e,[]);if(`refusal`in n)return n.refusal;let r=await re(t.app,n.email),i=await _(t.app).catch(()=>null);return t.audit?.({action:`forms.privacy.erase`,entityType:`form_submission`,metadata:{count:r.erased,forms:[...r.formIds],unfinished:r.more,failed:r.failed,unindexed:i}}),R({erased:r.erased,forms:r.formIds.length,more:r.more,failed:r.failed,unindexed:i},r.failed?500:200)},Ue=t({prefix:F,path:`search`,method:`POST`,permission:s,defaultRoles:[`admin`,`editor`],description:`Find this site’s submissions by one whole email address: { email }.`,descriptionKey:`Forms.permissions.routePrivacySearch`,tokens:`accept`,handler:Ne}),We=t({prefix:F,path:`export`,method:`POST`,permission:s,defaultRoles:[`admin`,`editor`],description:`Download one person’s submissions as CSV or JSON: { email, format }.`,descriptionKey:`Forms.permissions.routePrivacyExport`,tokens:`accept`,handler:Ve}),Ge=t({prefix:F,path:``,method:`DELETE`,permission:o,defaultRoles:[`admin`,`editor`],description:`Erase one person’s submissions for good: { email }. People only, no agents.`,descriptionKey:`Forms.permissions.routePrivacyErase`,tokens:{refuse:`personal-data`},handler:He});function Ke(){return e([Ue,We,Ge])}const H=`form-submissions`;function U(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`,"Cache-Control":`no-store`}})}const W=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function G(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function qe(e){return Array.isArray(e)}function K(e,t){if(!G(e))return null;let n=new Set([`form`,`ids`,...t]);if(Object.keys(e).some(e=>!n.has(e)))return null;let{form:r,ids:i}=e;if(typeof r!=`string`||!W.test(r)||!qe(i)||i.length===0||i.length>d)return null;let a=i.filter(e=>typeof e==`string`&&W.test(e));return a.length===i.length?{form:r,ids:a,rest:e}:null}async function q(e){let t=await C(e,8192);if(`tooLarge`in t)return{refusal:U({error:`body_too_large`},413)};let n=t.text;try{return{value:JSON.parse(n)}}catch{return{refusal:U({error:`invalid_json`},400)}}}async function J(e,t){let n=await e.getSystemClient(l).findById(t);return n==null?null:{name:typeof n.name==`string`?n.name:`form`,body:n.body}}const Je=async(e,t)=>{let n=new URL(e.url).searchParams.get(`form`)??``;if(!W.test(n)||await J(t.app,n)===null)return U({error:`not_found`},404);let r=(await ge(t.app,[n])).get(n);return U({total:r?.total??0,fresh:r?.fresh??0})},Ye=async(e,t)=>{let n=await q(e);if(`refusal`in n)return n.refusal;let r=K(n.value,[`status`]),i=r?.rest.status;if(r===null||!ce(i))return U({error:`Expected { form: uuid, ids: uuid[], status: new|read|archived }`},400);let{form:a}=r;if(await J(t.app,a)===null)return U({error:`not_found`},404);let o=await me(t.app,a,r.ids,i);return t.audit?.({action:`forms.submissions.status`,entityType:`form`,entityId:a,metadata:{status:i,count:o}}),U({changed:o})},Xe=async(e,t)=>{let n=await q(e);if(`refusal`in n)return n.refusal;let r=K(n.value,[]);if(r===null)return U({error:`Expected { form: uuid, ids: uuid[] }`},400);let{form:i}=r;if(await J(t.app,i)===null)return U({error:`not_found`},404);let a=await te(t.app,i,r.ids);return t.audit?.({action:`forms.submissions.delete`,entityType:`form`,entityId:i,metadata:{count:a}}),U({deleted:a})},Y={received:`Received (UTC)`,page:`Page`,status:`Status`,yes:`yes`,no:`no`,removed:`removed field`,statuses:{new:`New`,read:`Read`,archived:`Archived`}};function Ze(e,t){let n=e.Forms;for(let e of t){if(!G(n))return;n=n[e]}return typeof n==`string`&&n!==``?n:void 0}async function X(e){let t=await w(e??`en`),n=(e,n)=>Ze(t,e)??n;return{received:n([`submissions`,`csv`,`received`],Y.received),page:n([`submissions`,`csv`,`page`],Y.page),status:n([`submissions`,`csv`,`status`],Y.status),yes:n([`submissions`,`yes`],Y.yes),no:n([`submissions`,`no`],Y.no),removed:n([`submissions`,`removedField`],Y.removed),statuses:{new:n([`submissionStatus`,`new`],`New`),read:n([`submissionStatus`,`read`],`Read`),archived:n([`submissionStatus`,`archived`],`Archived`)}}}function Qe(e){return e.slice(0,16).replace(`T`,` `)}function $e(e,t){return e===void 0?``:typeof e==`boolean`?e?t.yes:t.no:e}const et=t({prefix:H,path:`export`,method:`GET`,permission:s,defaultRoles:[`admin`,`editor`],description:`Download a form’s submissions as CSV: ?form=<id> plus the screen’s filters.`,descriptionKey:`Forms.permissions.routeExport`,tokens:`accept`,handler:async(e,t)=>{let n=new URL(e.url),r=n.searchParams.get(`form`)??``;if(!W.test(r))return U({error:`not_found`},404);let i=await J(t.app,r);if(i===null)return U({error:`not_found`},404);let a={...fe(e=>n.searchParams.get(e)),until:new Date},o=n.searchParams.get(`uiLocale`)??t.locale,s=v(i.body),c=t.app,l=e=>D((e,t)=>le(c,r,a,{limit:t,offset:e}),{pageSize:m,...e!==void 0&&{onTruncated:e}});return O(async()=>{t.audit?.({action:`forms.submissions.export`,entityType:`form`,entityId:r,metadata:{filtered:_e(a).size>0}});let e=await X(o),n=new Map;for await(let e of l())h(s,[e],n);let u=he(s,n);return{filename:E(i.name),delimiter:T(o),header:[e.received,e.page,e.status,...u.map(t=>t.retired?`${t.label} (${e.removed})`:t.label)],rows:(async function*(){let t=l(e=>{c.logger.warn({formId:r,written:e},`forms: a submissions export reached the row cap — the file holds the newest rows only`)});for await(let n of t)yield[Qe(n.createdAt),n.pagePath??``,e.statuses[n.status]??n.status,...u.map(t=>$e(n.data[t.key],e))]})()}})}}),tt=t({prefix:H,path:`summary`,method:`GET`,permission:s,defaultRoles:[`admin`,`editor`],description:`How many submissions a form holds and how many are new: ?form=<id>.`,descriptionKey:`Forms.permissions.routeSummary`,tokens:`accept`,handler:Je}),nt=t({prefix:H,path:``,method:`PATCH`,permission:o,defaultRoles:[`admin`,`editor`],description:`Mark submissions of a form new, read or archived: { form, ids, status }.`,descriptionKey:`Forms.permissions.routeStatus`,tokens:`accept`,handler:Ye}),rt=t({prefix:H,path:``,method:`DELETE`,permission:o,defaultRoles:[`admin`,`editor`],description:`Delete submissions of a form for good: { form, ids }.`,descriptionKey:`Forms.permissions.routeDelete`,tokens:`accept`,confirm:!0,handler:Xe});function it(){return e([et,tt,nt,rt])}const Z=`${l.name}:view`,at=async e=>(await import(`@murumets-ee/forms-ui/pages`)).FormsListPage(e),ot=async e=>(await import(`@murumets-ee/forms-ui/pages`)).FormCanvasPage(e),st=async e=>(await import(`@murumets-ee/forms-ui/pages`)).SubmissionsPage(e),ct=async e=>(await import(`@murumets-ee/forms-ui/pages`)).PrivacyRequestsPage(e),Q=`forms:retention`,$=`23 3 * * *`;function lt(){return n({name:`@murumets-ee/forms`,requires:[`@murumets-ee/content`,`@murumets-ee/settings`,`@murumets-ee/notifications`,`@murumets-ee/queue`],server:{entities:[l,u],tables:{...oe},routes:[...S(),...it(),...Ke(),...je()],init:async e=>{if(!e.plugins.has(`@murumets-ee/queue`))return;let{defineJob:t,registerJob:n}=await import(`@murumets-ee/queue/client`);n(t({name:Q,schedule:$}),async()=>{let{runAsCli:t,runInEverySite:n}=await import(`@murumets-ee/core`),{runFormsRetention:r}=await import(`@murumets-ee/forms`),i=await t(()=>n(()=>r(e),{app:e,label:Q})),a=i.reduce((e,t)=>e+t.removed,0);e.logger.info({sites:i.length,removed:a,unfinished:i.filter(e=>e.unfinished).length,backfilled:i.reduce((e,t)=>e+t.backfilled,0)},`forms: daily clean-up of expired submissions finished`)});let{runAsCli:r}=await import(`@murumets-ee/core`),{runFormEmailJob:i,sendFormEmailJob:a}=await import(`@murumets-ee/forms/email-jobs`);n(a,async t=>{await r(async()=>{let n=`en`;try{let{getNotificationsConfig:e}=await import(`@murumets-ee/notifications/plugin`);n=e().defaultLocale}catch{e.logger.warn({outcome:`no_default_locale`},`forms: notifications default locale unavailable — using en`)}await i(t.payload,{app:e,logger:e.logger,defaultLocale:n})})})}},contributes:{"blocks.blocks":[...Se,ye],"blocks.environmentBases":[be],"blocks.environments":xe},shared:{features:x,notifications:[...ie],settings:[ae],messages:w},adminUi:{canvasHeaderActions:{[l.name]:Ce},pages:{FormsListPage:at,FormCanvasPage:ot,SubmissionsPage:st,PrivacyRequestsPage:ct},sidebar:[{id:`forms`,group:`Content`,label:`Forms`,href:`/admin/forms`,iconName:`clipboard-list`,requires:Z}],defaultRoutes:[{path:`forms`,factory:`FormsListPage`,page:{requires:Z,defaultRoles:[`admin`,`editor`],description:`List this site’s forms and create new ones.`,descriptionKey:`Forms.permissions.list`,tokens:`accept`}},{path:`forms/privacy`,factory:`PrivacyRequestsPage`,page:{requires:s,defaultRoles:[`admin`,`editor`],description:`Find one person’s submissions by email address, to export or erase them.`,descriptionKey:`Forms.permissions.privacy`,tokens:`accept`}},{path:`forms/[id]`,factory:`FormCanvasPage`,page:{requires:i,defaultRoles:[`admin`,`editor`],description:`Edit a form on its canvas.`,descriptionKey:`Forms.permissions.canvas`,tokens:`accept`}},{path:`forms/[id]/submissions`,factory:`SubmissionsPage`,page:{requires:s,defaultRoles:[`admin`,`editor`],description:`Read and export what visitors sent through a form.`,descriptionKey:`Forms.permissions.submissions`,tokens:`accept`}},{path:`form`,factory:`FormsListPage`},{path:`form/[id]`,factory:`FormCanvasPage`}]}})}export{Q as FORMS_RETENTION_JOB,$ as FORMS_RETENTION_SCHEDULE,lt 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), 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"}
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":["MAX_BODY_BYTES","UUID_RE","json","isRecord","readBody","loadForm","MAX_BODY_BYTES","json","isRecord","message","exportCopy","csvTime","exportRoute"],"sources":["../src/email-routes.ts","../src/privacy-routes.ts","../src/submission-routes.ts","../src/plugin.ts"],"sourcesContent":["/**\n * A form's email settings and delivery Retry — the admin API behind the form\n * panel's Emails tab and the Submissions screen (forms D006). Prefix\n * `form-emails`:\n *\n * - `PATCH form-emails` `{ form, recipients, autoReplyEnabled, autoReplyField }`\n * — who is told of each answer, and whether the visitor is thanked.\n * - `POST form-emails/retry` `{ form, submission, kind }` — queue a failed (or\n * skipped) email again.\n *\n * **Authorization.** `PATCH` is `forms:manage` (building the form), `retry` is\n * `form_submissions:manage` (tending its submissions). The tables are reached\n * through `@murumets-ee/forms`' functions with the app's own database handle,\n * after the route has authorized and the form has been shown to belong to THIS\n * site — another site's id reads as \"not found\" (C2).\n *\n * **Validation.** Strict bodies (any other key is malformed); every recipient is\n * a plain `local@domain.tld` ({@link parseRecipients}), at most ten; the\n * auto-reply field must be an email field of the form AS IT IS NOW.\n *\n * **Why recipients are not in the form.** A form's published tree is delivered to\n * every visitor, so an address in a block prop would be published (F017). They\n * live in `forms_email_settings`, which no public route reads.\n *\n * **Agents.** An agent whose role holds the permission may call them. Changing\n * who receives every answer is the one call here that could redirect visitors'\n * data, so it is `confirm: true`: an agent under a human-approves policy has it\n * parked for a person. A Retry only re-sends to recipients already set.\n *\n * **Personal data (C5).** An audit row names the form and counts. Never an\n * address.\n */\n\nimport {\n type AdminRoute,\n type AdminRouteHandler,\n combineAdminRoutes,\n defineAdminRoute,\n requireRequestSite,\n type ToolkitApp,\n} from '@murumets-ee/core'\nimport {\n compileForm,\n enqueueFormEmail,\n FORM_SUBMISSION_ENTITY,\n FORM_SUBMISSIONS_MANAGE,\n FORMS_MANAGE,\n FORMS_SETTINGS_NAMESPACE,\n Form,\n FormSubmission,\n isEmailKind,\n parseRecipients,\n readCappedBody,\n readDeliveries,\n writeEmailSettings,\n} from '@murumets-ee/forms'\n\nexport const EMAIL_ROUTES_PREFIX = 'form-emails'\n\nconst MAX_BODY_BYTES = 8192\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\nfunction json(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },\n })\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nasync function readBody(req: Request): Promise<{ value: unknown } | { refusal: Response }> {\n const body = await readCappedBody(req, MAX_BODY_BYTES)\n if ('tooLarge' in body) return { refusal: json({ error: 'body_too_large' }, 413) }\n try {\n return { value: JSON.parse(body.text) as unknown }\n } catch {\n return { refusal: json({ error: 'invalid_json' }, 400) }\n }\n}\n\n/** Exactly these keys, or `null`. */\nfunction strict(value: unknown, keys: readonly string[]): Record<string, unknown> | null {\n if (!isRecord(value)) return null\n const allowed = new Set(keys)\n return Object.keys(value).every((key) => allowed.has(key)) ? value : null\n}\n\n/** This site's form `id` (system read, confined by the site), or `null`. */\nasync function loadForm(app: ToolkitApp, id: string): Promise<{ body: unknown } | null> {\n const row: Record<string, unknown> | null | undefined = await app\n .getSystemClient(Form)\n .findById(id)\n return row === null || row === undefined ? null : { body: row.body }\n}\n\nexport const handleSaveEmails: AdminRouteHandler = async (req, ctx) => {\n const body = await readBody(req)\n if ('refusal' in body) return body.refusal\n const parsed = strict(body.value, ['form', 'recipients', 'autoReplyEnabled', 'autoReplyField'])\n const { form, recipients, autoReplyEnabled, autoReplyField } = parsed ?? {}\n if (\n parsed === null ||\n typeof form !== 'string' ||\n !UUID_RE.test(form) ||\n typeof autoReplyEnabled !== 'boolean' ||\n !(autoReplyField === null || typeof autoReplyField === 'string')\n ) {\n return json(\n {\n error:\n 'Expected { form: uuid, recipients: string[], autoReplyEnabled: boolean, autoReplyField: string|null }',\n },\n 400,\n )\n }\n const verdict = parseRecipients(recipients)\n if (!verdict.ok) {\n return json(\n { error: 'invalid_recipients', invalid: verdict.invalid, tooMany: verdict.tooMany },\n 400,\n )\n }\n const loaded = await loadForm(ctx.app, form)\n if (loaded === null) return json({ error: 'not_found' }, 404)\n\n // The auto-reply goes to ONE email field of the form as it is now.\n const fields = compileForm(loaded.body)?.fields ?? []\n const emailKeys = fields.filter((field) => field.kind === 'email').map((field) => field.key)\n if (autoReplyField !== null && !emailKeys.includes(autoReplyField)) {\n return json({ error: 'invalid_auto_reply_field' }, 400)\n }\n if (autoReplyEnabled && autoReplyField === null) {\n return json({ error: 'auto_reply_needs_field' }, 400)\n }\n\n await writeEmailSettings(ctx.app, form, {\n recipients: verdict.recipients,\n autoReplyEnabled,\n autoReplyField,\n })\n ctx.audit?.({\n action: 'forms.emails.update',\n entityType: 'form',\n entityId: form,\n metadata: { recipients: verdict.recipients.length, autoReply: autoReplyEnabled },\n })\n return json({ recipients: verdict.recipients, autoReplyEnabled, autoReplyField })\n}\n\n/** A queued email this old has lost its job (worker down, process died). */\nconst STUCK_AFTER_MS = 5 * 60 * 1000\n/** How long a timed-out auto-reply holds its slot — the guard's window. */\nconst GUARD_WINDOW_MS = 60 * 60 * 1000\n/** Skips a changed setting can cure; the others (hourly limit, bad address, gone) would be refused again. */\nconst RETRYABLE_SKIPS: readonly string[] = ['no_recipients', 'disabled']\n\n/** Where a Retry can change the answer — the screen's `canRetry` and this route agree. */\nfunction isRetryable(\n row: { kind: string; status: string; errorClass: string | null; updatedAt: string },\n now: number,\n): boolean {\n if (row.status === 'failed') {\n // A timed-out auto-reply holds its hourly slot (the mail may still arrive):\n // a Retry inside that hour would only be refused.\n const held =\n row.kind === 'autoReply' &&\n row.errorClass === 'timeout' &&\n now - new Date(row.updatedAt).getTime() < GUARD_WINDOW_MS\n return !held\n }\n if (row.status === 'queued') return now - new Date(row.updatedAt).getTime() > STUCK_AFTER_MS\n return (\n row.status === 'skipped' && row.errorClass !== null && RETRYABLE_SKIPS.includes(row.errorClass)\n )\n}\n\nexport const handleRetry: AdminRouteHandler = async (req, ctx) => {\n const body = await readBody(req)\n if ('refusal' in body) return body.refusal\n const parsed = strict(body.value, ['form', 'submission', 'kind'])\n const { form, submission, kind } = parsed ?? {}\n if (\n parsed === null ||\n typeof form !== 'string' ||\n !UUID_RE.test(form) ||\n typeof submission !== 'string' ||\n !UUID_RE.test(submission) ||\n !isEmailKind(kind)\n ) {\n return json(\n { error: 'Expected { form: uuid, submission: uuid, kind: notification|autoReply }' },\n 400,\n )\n }\n if ((await loadForm(ctx.app, form)) === null) return json({ error: 'not_found' }, 404)\n // The submission must be this form's — and this site's, which the system\n // client's site binding already guarantees.\n const row: Record<string, unknown> | null | undefined = await ctx.app\n .getSystemClient(FormSubmission)\n .findById(submission)\n if (row === null || row === undefined || String(row.form) !== form) {\n return json({ error: 'not_found' }, 404)\n }\n // The server holds the same line the screen draws: an email that went is not\n // sent again by a replayed request (every call would mail every recipient the\n // full set of answers), and a fresh queued one is still on its way.\n const current = (await readDeliveries(ctx.app, form, [submission])).get(submission) ?? []\n const delivery = current.find((entry) => entry.kind === kind)\n if (delivery !== undefined && !isRetryable(delivery, Date.now())) {\n return json({ error: 'not_retryable', status: delivery.status }, 409)\n }\n const site = await requireRequestSite(FORMS_SETTINGS_NAMESPACE, ctx.app)\n\n const queued = await enqueueFormEmail(ctx.app, {\n site,\n formId: form,\n submissionId: submission,\n kind,\n })\n ctx.audit?.({\n action: 'forms.emails.retry',\n entityType: FORM_SUBMISSION_ENTITY,\n entityId: submission,\n metadata: { kind, queued },\n })\n // `readEmailSettings` is not consulted: a Retry sends to whoever is set NOW,\n // and the job says \"skipped: no_recipients\" if nobody is.\n return json({ queued })\n}\n\nconst saveRoute = defineAdminRoute({\n prefix: EMAIL_ROUTES_PREFIX,\n path: '',\n method: 'PATCH',\n permission: FORMS_MANAGE,\n defaultRoles: ['admin', 'editor'],\n description: 'Set a form’s email recipients and auto-reply: { form, recipients, … }.',\n descriptionKey: 'Forms.permissions.routeEmails',\n tokens: 'accept',\n // Redirecting every answer to a new address is the one call here that could\n // move visitors' data; an agent under a human-approves policy waits for a person.\n confirm: true,\n handler: handleSaveEmails,\n})\n\nconst retryRoute = defineAdminRoute({\n prefix: EMAIL_ROUTES_PREFIX,\n path: 'retry',\n method: 'POST',\n permission: FORM_SUBMISSIONS_MANAGE,\n defaultRoles: ['admin', 'editor'],\n description: 'Queue a failed form email again: { form, submission, kind }.',\n descriptionKey: 'Forms.permissions.routeEmailRetry',\n tokens: 'accept',\n handler: handleRetry,\n})\n\n/** The form-email admin routes (`server.routes`). */\nexport function emailRoutes(): AdminRoute[] {\n return combineAdminRoutes([saveRoute, retryRoute])\n}\n","/**\n * The Privacy requests screen's admin API (forms D007, W5) — prefix\n * `form-privacy`:\n *\n * - `POST form-privacy/search` `{ email }` — which of this site's submissions\n * carry that address: form, time and page of each, and the total.\n * - `POST form-privacy/export` `{ email, format, uiLocale? }` — all of them,\n * with their answers under the labels they had, as CSV or JSON.\n * - `DELETE form-privacy` `{ email }` — erase all of them, for good.\n *\n * **The address travels in the request BODY, never the URL** — which is why\n * the two reads are POSTs. A query string is written to access logs, proxy\n * logs and browser history; an address there would be personal data in a log\n * (C5), about the very person asking to be forgotten.\n *\n * **Authorization.** Each method declares its permission, checked by\n * `defineAdminRoute` before the handler runs: finding and exporting is\n * `form_submissions:view`, erasing is `form_submissions:manage` (forms D011).\n * The rows are then reached through `@murumets-ee/forms`' privacy functions,\n * which use an elevated client — sanctioned because the route HAS authorized,\n * on a different resource, and nothing from the body is forwarded into a\n * write: the one input is an address, parsed to a whole address and used as a\n * bound parameter of a lookup. The site is the request's, so another site's\n * submissions are never found (C2).\n *\n * **Erasing names the address again.** The erase takes `{ email }`, not ids\n * from the browser, so what is erased is what the server finds for that\n * address at that moment — a submission that arrived since the search goes\n * too, and a browser cannot name rows of its own choosing.\n *\n * **Agents.** An agent whose role holds the permission may FIND and EXPORT\n * (`tokens: 'accept'`), as it may read a form's Submissions. ERASING is closed\n * to every token-derived door (no `tokens` choice, which is deny): an erase a\n * person has to approve is PARKED with its request body, and that body is the\n * address — stored in the approvals table, and shown on the Approvals page,\n * about the very person asking to be forgotten. A person erases; an agent can\n * prepare the request by finding and exporting.\n *\n * **What a search cannot see.** A submission stored before the email lookup\n * existed is not findable until the nightly clean-up has filled its lookup\n * in. Search and erase answer `unindexed` — how many of this site's\n * submissions that is — and the screen says so, so \"nothing found\" is never\n * mistaken for \"nothing stored\".\n *\n * **Audit (C5).** A row names the action, a count and the form ids. Never the\n * address, never an answer.\n */\n\nimport {\n type CsvExport,\n csvDelimiterFor,\n csvFilename,\n pagedExportRows,\n runCsvExport,\n} from '@murumets-ee/admin-ui/csv-export'\nimport {\n type AdminRoute,\n type AdminRouteHandler,\n combineAdminRoutes,\n defineAdminRoute,\n type ToolkitApp,\n} from '@murumets-ee/core'\nimport {\n countSubmissionsByEmail,\n countUnindexedSubmissions,\n eraseSubmissionsByEmail,\n FORM_SUBMISSIONS_MANAGE,\n FORM_SUBMISSIONS_VIEW,\n findSubmissionsByEmail,\n formNames,\n type PersonSubmission,\n PRIVACY_EXPORT_MAX,\n PRIVACY_LIST_MAX,\n parsePrivacyEmail,\n readCappedBody,\n SUBMISSIONS_READ_MAX,\n type SubmissionAnswer,\n} from '@murumets-ee/forms'\nimport { getFormsMessages } from '@murumets-ee/forms/i18n'\nimport type { PrivacyMatch } from './privacy-match.js'\n\nexport const PRIVACY_ROUTES_PREFIX = 'form-privacy'\n\n/** Says whether an export stopped at its row cap — read by the screen. */\nexport const TRUNCATED_HEADER = 'X-Lumi-Export-Truncated'\n\n/** Exports being READ into memory at once in this process; a third is told to wait. */\nconst MAX_EXPORT_READS = 2\nlet reading = 0\n\n/** For tests: how many privacy exports are being read right now. */\nexport function _readingPrivacyExports(): number {\n return reading\n}\n\nfunction busy(): Response {\n return new Response(JSON.stringify({ error: 'export_busy', code: 'export_busy' }), {\n status: 503,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-store',\n 'Retry-After': '10',\n },\n })\n}\n\n/** A body is an address and two short words. */\nconst MAX_BODY_BYTES = 2048\n\nfunction json(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },\n })\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * The body's address, with exactly the keys named beside it — strict, so a\n * body carrying anything else is malformed rather than half-read.\n */\nasync function readRequest(\n req: Request,\n extraKeys: readonly string[],\n): Promise<{ email: string; rest: Record<string, unknown> } | { refusal: Response }> {\n // Capped as the bytes arrive — a header can lie or be absent.\n const body = await readCappedBody(req, MAX_BODY_BYTES)\n if ('tooLarge' in body) return { refusal: json({ error: 'body_too_large' }, 413) }\n let value: unknown\n try {\n value = JSON.parse(body.text)\n } catch {\n return { refusal: json({ error: 'invalid_json' }, 400) }\n }\n const allowed = new Set(['email', ...extraKeys])\n if (!isRecord(value) || Object.keys(value).some((key) => !allowed.has(key))) {\n return { refusal: json({ error: 'invalid_body' }, 400) }\n }\n const email = parsePrivacyEmail(value.email)\n // The answer never echoes what was sent.\n if (email === null)\n return { refusal: json({ error: 'invalid_email', code: 'invalid_email' }, 400) }\n return { email, rest: value }\n}\n\n// ---------------------------------------------------------------------------\n// POST search — what the screen lists\n// ---------------------------------------------------------------------------\n\nexport const handlePrivacySearch: AdminRouteHandler = async (req, ctx) => {\n const request = await readRequest(req, [])\n if ('refusal' in request) return request.refusal\n const { email } = request\n const [total, rows, unindexed] = await Promise.all([\n countSubmissionsByEmail(ctx.app, email),\n findSubmissionsByEmail(ctx.app, email, { limit: PRIVACY_LIST_MAX, offset: 0 }),\n countUnindexedSubmissions(ctx.app),\n ])\n const names = await formNames(\n ctx.app,\n rows.map((row) => row.formId),\n )\n const items: PrivacyMatch[] = rows.map((row) => ({\n id: row.id,\n formId: row.formId,\n formName: names.get(row.formId) ?? null,\n createdAt: row.createdAt,\n pagePath: row.pagePath,\n }))\n ctx.audit?.({\n action: 'forms.privacy.search',\n entityType: 'form_submission',\n metadata: { count: total, forms: [...new Set(rows.map((row) => row.formId))], unindexed },\n })\n return json({ total, items, unindexed, exportMax: PRIVACY_EXPORT_MAX })\n}\n\n// ---------------------------------------------------------------------------\n// POST export — everything the person sent, with labels\n// ---------------------------------------------------------------------------\n\nconst FORMATS = ['csv', 'json'] as const\ntype ExportFormat = (typeof FORMATS)[number]\n\ninterface ExportCopy {\n form: string\n received: string\n page: string\n status: string\n yes: string\n no: string\n deletedForm: string\n statuses: Record<string, string>\n}\n\n/** `Forms.<...path>` in a catalogue, when it is a non-empty string. */\nfunction message(catalogue: Record<string, unknown>, path: readonly string[]): string | undefined {\n let node: unknown = catalogue.Forms\n for (const key of path) {\n if (!isRecord(node)) return undefined\n node = node[key]\n }\n return typeof node === 'string' && node !== '' ? node : undefined\n}\n\nasync function exportCopy(locale: string | undefined): Promise<ExportCopy> {\n // The loader answers English for a locale it does not ship, so an unknown\n // `uiLocale` cannot choose a file to import.\n const catalogue = await getFormsMessages(locale ?? 'en')\n const pick = (path: readonly string[], fallback: string) => message(catalogue, path) ?? fallback\n return {\n form: pick(['privacy', 'csv', 'form'], 'Form'),\n received: pick(['privacy', 'csv', 'received'], 'Received (UTC)'),\n page: pick(['privacy', 'csv', 'page'], 'Page'),\n status: pick(['privacy', 'csv', 'status'], 'Status'),\n yes: pick(['submissions', 'yes'], 'yes'),\n no: pick(['submissions', 'no'], 'no'),\n deletedForm: pick(['privacy', 'deletedForm'], 'deleted'),\n statuses: {\n new: pick(['submissionStatus', 'new'], 'New'),\n read: pick(['submissionStatus', 'read'], 'Read'),\n archived: pick(['submissionStatus', 'archived'], 'Archived'),\n },\n }\n}\n\n/** The label an answer had when it was sent; its key when the snapshot has none. */\nfunction labelOf(row: PersonSubmission, key: string): string {\n const label = row.fields[key]?.label\n return label !== undefined && label !== '' ? label : key\n}\n\n/** `{ label: answers }` of one submission — two fields that shared a label share its cell. */\nfunction answersByLabel(row: PersonSubmission): Map<string, SubmissionAnswer[]> {\n const byLabel = new Map<string, SubmissionAnswer[]>()\n for (const [key, value] of Object.entries(row.data)) {\n const label = labelOf(row, key)\n byLabel.set(label, [...(byLabel.get(label) ?? []), value])\n }\n return byLabel\n}\n\nfunction cell(values: readonly SubmissionAnswer[] | undefined, copy: ExportCopy): string | number {\n if (values === undefined || values.length === 0) return ''\n const words = values.map((value) =>\n typeof value === 'boolean' ? (value ? copy.yes : copy.no) : value,\n )\n const only = words[0]\n return words.length === 1 && only !== undefined ? only : words.join(' | ')\n}\n\n/** `2026-10-06 14:02` — a form a spreadsheet reads as a date, in UTC as the header says. */\nfunction csvTime(iso: string): string {\n return iso.slice(0, 16).replace('T', ' ')\n}\n\n/** The person's submissions, page by page, fixed at `until`, at most the export cap. */\nfunction personRows(\n app: ToolkitApp,\n email: string,\n until: Date,\n onTruncated?: (written: number) => void,\n): AsyncGenerator<PersonSubmission, void, undefined> {\n return pagedExportRows<PersonSubmission>(\n (offset, limit) => findSubmissionsByEmail(app, email, { limit, offset, until }),\n {\n pageSize: SUBMISSIONS_READ_MAX,\n maxRows: PRIVACY_EXPORT_MAX,\n ...(onTruncated !== undefined && { onTruncated }),\n },\n )\n}\n\nexport const handlePrivacyExport: AdminRouteHandler = async (req, ctx) => {\n const request = await readRequest(req, ['format', 'uiLocale'])\n if ('refusal' in request) return request.refusal\n const { email, rest } = request\n const format: ExportFormat | undefined = FORMATS.find((name) => name === rest.format)\n if (format === undefined) return json({ error: 'invalid_format' }, 400)\n const uiLocale =\n typeof rest.uiLocale === 'string' && rest.uiLocale.length <= 35 ? rest.uiLocale : ctx.locale\n const app = ctx.app\n // The whole set is read ONCE, into memory, before a byte is written: the CSV\n // header needs every label, and a second pass over a set that an erase or\n // the clean-up is changing could meet a row the first pass never saw. So the\n // read is bounded twice — PRIVACY_EXPORT_MAX rows, and MAX_EXPORT_READS\n // reads at a time in this process. (The rows then live until the download\n // ends: what is bounded is the querying, and each file's size.)\n if (reading >= MAX_EXPORT_READS) return busy()\n reading += 1\n const until = new Date()\n const rows: PersonSubmission[] = []\n let truncated = false\n let names: Map<string, string>\n try {\n for await (const row of personRows(app, email, until, () => {\n truncated = true\n })) {\n rows.push(row)\n }\n names = await formNames(app, [...new Set(rows.map((row) => row.formId))])\n } finally {\n reading -= 1\n }\n if (truncated) {\n // A count — nothing a visitor sent, and not the address.\n app.logger.warn(\n { written: rows.length },\n 'forms: a privacy export reached the row cap — the file holds the newest rows only',\n )\n }\n // Written once the file is actually on its way — a refused export (every CSV\n // slot taken) must not be recorded as one somebody received.\n const audit = (): void => {\n ctx.audit?.({\n action: 'forms.privacy.export',\n entityType: 'form_submission',\n metadata: {\n count: rows.length,\n forms: [...new Set(rows.map((row) => row.formId))],\n format,\n truncated,\n },\n })\n }\n\n if (format === 'json') {\n const body = JSON.stringify(\n {\n exportedAt: until.toISOString(),\n truncated,\n submissions: rows.map((row) => ({\n form: names.get(row.formId) ?? null,\n receivedAt: row.createdAt,\n page: row.pagePath,\n language: row.locale,\n status: row.status,\n answers: Object.entries(row.data).map(([key, value]) => ({\n field: labelOf(row, key),\n key,\n answer: value,\n })),\n consent: Object.values(row.consent),\n })),\n },\n null,\n 2,\n )\n audit()\n return new Response(body, {\n status: 200,\n headers: {\n 'Content-Type': 'application/json; charset=utf-8',\n 'Content-Disposition': `attachment; filename=\"${csvFilename('privacy-request').replace(/\\.csv$/, '.json')}\"`,\n 'Cache-Control': 'private, no-store',\n 'X-Content-Type-Options': 'nosniff',\n [TRUNCATED_HEADER]: String(truncated),\n },\n })\n }\n\n const copy = await exportCopy(uiLocale)\n const labels: string[] = []\n const seen = new Set<string>()\n for (const row of rows) {\n for (const key of Object.keys(row.data)) {\n const label = labelOf(row, key)\n if (seen.has(label)) continue\n seen.add(label)\n labels.push(label)\n }\n }\n const res = await runCsvExport(\n (): CsvExport => ({\n filename: csvFilename('privacy-request'),\n delimiter: csvDelimiterFor(uiLocale),\n header: [copy.form, copy.received, copy.page, copy.status, ...labels],\n rows: rows.map((row) => {\n const answers = answersByLabel(row)\n return [\n names.get(row.formId) ?? copy.deletedForm,\n csvTime(row.createdAt),\n row.pagePath ?? '',\n copy.statuses[row.status] ?? row.status,\n ...labels.map((label) => cell(answers.get(label), copy)),\n ]\n }),\n }),\n )\n if (res.ok) {\n audit()\n res.headers.set(TRUNCATED_HEADER, String(truncated))\n }\n return res\n}\n\n// ---------------------------------------------------------------------------\n// DELETE — erase, for good\n// ---------------------------------------------------------------------------\n\nexport const handlePrivacyErase: AdminRouteHandler = async (req, ctx) => {\n const request = await readRequest(req, [])\n if ('refusal' in request) return request.refusal\n // Never throws part-way: what went before a failed batch is reported, so it\n // reaches the audit row below either way.\n const result = await eraseSubmissionsByEmail(ctx.app, request.email)\n const unindexed = await countUnindexedSubmissions(ctx.app).catch(() => null)\n ctx.audit?.({\n action: 'forms.privacy.erase',\n entityType: 'form_submission',\n // How many, from which forms — never whose (D007).\n metadata: {\n count: result.erased,\n forms: [...result.formIds],\n unfinished: result.more,\n failed: result.failed,\n unindexed,\n },\n })\n return json(\n {\n erased: result.erased,\n forms: result.formIds.length,\n more: result.more,\n failed: result.failed,\n unindexed,\n },\n result.failed ? 500 : 200,\n )\n}\n\n// ---------------------------------------------------------------------------\n// Declarations\n// ---------------------------------------------------------------------------\n\nconst searchRoute = defineAdminRoute({\n prefix: PRIVACY_ROUTES_PREFIX,\n path: 'search',\n method: 'POST',\n permission: FORM_SUBMISSIONS_VIEW,\n defaultRoles: ['admin', 'editor'],\n // The request and the answer are in this file's header.\n description: 'Find this site’s submissions by one whole email address: { email }.',\n descriptionKey: 'Forms.permissions.routePrivacySearch',\n tokens: 'accept',\n handler: handlePrivacySearch,\n})\n\nconst exportRoute = defineAdminRoute({\n prefix: PRIVACY_ROUTES_PREFIX,\n path: 'export',\n method: 'POST',\n permission: FORM_SUBMISSIONS_VIEW,\n defaultRoles: ['admin', 'editor'],\n // The request and the answer are in this file's header.\n description: 'Download one person’s submissions as CSV or JSON: { email, format }.',\n descriptionKey: 'Forms.permissions.routePrivacyExport',\n tokens: 'accept',\n handler: handlePrivacyExport,\n})\n\nconst eraseRoute = defineAdminRoute({\n prefix: PRIVACY_ROUTES_PREFIX,\n path: '',\n method: 'DELETE',\n permission: FORM_SUBMISSIONS_MANAGE,\n defaultRoles: ['admin', 'editor'],\n // The request and the answer are in this file's header.\n description: 'Erase one person’s submissions for good: { email }. People only, no agents.',\n descriptionKey: 'Forms.permissions.routePrivacyErase',\n // Closed to every token-derived door, and said so (agent-access PR14). A\n // parked erase would store its body — the address — in the approvals table\n // (see the header). A person erases, from their own session.\n tokens: { refuse: 'personal-data' },\n handler: handlePrivacyErase,\n})\n\n/** The Privacy requests screen's admin routes (`server.routes`). */\nexport function privacyRoutes(): AdminRoute[] {\n return combineAdminRoutes([searchRoute, exportRoute, eraseRoute])\n}\n","/**\n * The Submissions screen's admin API (forms D005, W4) — prefix\n * `form-submissions`:\n *\n * - `GET form-submissions/export?form=<id>&…filters` — the filtered set as a CSV.\n * - `GET form-submissions/summary?form=<id>` — `{ total, fresh }`, for the\n * \"Submissions (3 new)\" link on a Form block's panel (W3).\n * - `PATCH form-submissions` `{ form, ids, status }` — mark new / read / archived.\n * - `DELETE form-submissions` `{ form, ids }` — delete for good.\n *\n * (The screen's own rows are read on the server by the page; there is no list\n * route to keep in step with it.)\n *\n * **Authorization.** Each method declares its permission, checked by\n * `defineAdminRoute` before the handler runs: reading and exporting is\n * `form_submissions:view`, changing and deleting is `form_submissions:manage`.\n * The rows are then reached through `@murumets-ee/forms`' submission functions,\n * which use an elevated client — sanctioned here because the route HAS\n * authorized, on a different resource, and because nothing from the request\n * body is forwarded into a write: a status is one of three literals and ids\n * are UUIDs, both validated below. The site is the request's, and every query\n * names the form, so another site's form id reads as \"not found\" (C2).\n *\n * **Agents.** An agent whose role holds the permission may use these routes\n * (`tokens: 'accept'`), as it may the support inbox (agent-access D021);\n * deleting is `confirm: true`, so an agent under a human-approves policy has\n * its delete parked for a person.\n *\n * **Personal data (C5).** An audit row names the form, the action and a count.\n * Never an answer.\n *\n * Lives in forms-ui, not `@murumets-ee/forms`, because the export answers\n * through admin-ui's CSV writer and the forms package does not depend on\n * admin-ui. Reached from the jiti-loaded plugin entry, so nothing here imports\n * a `server-only` module.\n */\n\nimport {\n type CsvExport,\n csvDelimiterFor,\n csvFilename,\n pagedExportRows,\n runCsvExport,\n} from '@murumets-ee/admin-ui/csv-export'\nimport {\n type AdminRoute,\n type AdminRouteHandler,\n combineAdminRoutes,\n defineAdminRoute,\n type ToolkitApp,\n} from '@murumets-ee/core'\nimport {\n collectRetiredColumns,\n currentFormFields,\n deleteSubmissions,\n FORM_SUBMISSIONS_MANAGE,\n FORM_SUBMISSIONS_VIEW,\n Form,\n isSubmissionStatus,\n listSubmissionRows,\n MAX_SUBMISSION_BULK,\n parseSubmissionFilters,\n readCappedBody,\n SUBMISSIONS_READ_MAX,\n type SubmissionAnswer,\n type SubmissionRow,\n setSubmissionStatus,\n submissionColumns,\n submissionCounts,\n submissionFilterParams,\n} from '@murumets-ee/forms'\nimport { getFormsMessages } from '@murumets-ee/forms/i18n'\n\nexport const SUBMISSION_ROUTES_PREFIX = 'form-submissions'\n\n/** A body is a form id, a status and at most a hundred ids. */\nconst MAX_BODY_BYTES = 8192\n\nfunction json(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },\n })\n}\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\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** `Array.isArray` on `unknown` narrows to `any[]`; this keeps the elements `unknown`. */\nfunction isUnknownArray(value: unknown): value is unknown[] {\n return Array.isArray(value)\n}\n\n/**\n * `{ form, ids, ...extra }` with exactly the keys named — strict, so a body\n * carrying anything else is malformed rather than half-read. `null` otherwise.\n */\nfunction parseTarget(\n value: unknown,\n extraKeys: readonly string[],\n): { form: string; ids: string[]; rest: Record<string, unknown> } | null {\n if (!isRecord(value)) return null\n const allowed = new Set(['form', 'ids', ...extraKeys])\n if (Object.keys(value).some((key) => !allowed.has(key))) return null\n const { form, ids } = value\n if (typeof form !== 'string' || !UUID_RE.test(form)) return null\n if (!isUnknownArray(ids) || ids.length === 0 || ids.length > MAX_SUBMISSION_BULK) return null\n const clean = ids.filter((id): id is string => typeof id === 'string' && UUID_RE.test(id))\n if (clean.length !== ids.length) return null\n return { form, ids: clean, rest: value }\n}\n\n/** The request body as JSON, or the refusal to answer with. */\nasync function readBody(req: Request): Promise<{ value: unknown } | { refusal: Response }> {\n // Capped as the bytes arrive — a header can lie or be absent.\n const body = await readCappedBody(req, MAX_BODY_BYTES)\n if ('tooLarge' in body) return { refusal: json({ error: 'body_too_large' }, 413) }\n const raw = body.text\n try {\n const value: unknown = JSON.parse(raw)\n return { value }\n } catch {\n return { refusal: json({ error: 'invalid_json' }, 400) }\n }\n}\n\n/**\n * This site's form `id`, or `null`. Read with an elevated client for the same\n * reason the submissions are: the route's own permission is what authorized\n * seeing this form's name and fields, and the site still confines the read —\n * another site's id is not found.\n */\nasync function loadForm(\n app: ToolkitApp,\n id: string,\n): Promise<{ name: string; body: unknown } | null> {\n const row: Record<string, unknown> | null | undefined = await app\n .getSystemClient(Form)\n .findById(id)\n if (row === null || row === undefined) return null\n return { name: typeof row.name === 'string' ? row.name : 'form', body: row.body }\n}\n\n// ---------------------------------------------------------------------------\n// GET summary — two numbers, for the link on a Form block's panel\n// ---------------------------------------------------------------------------\n\nexport const handleSummary: AdminRouteHandler = async (req, ctx) => {\n const formId = new URL(req.url).searchParams.get('form') ?? ''\n if (!UUID_RE.test(formId)) return json({ error: 'not_found' }, 404)\n if ((await loadForm(ctx.app, formId)) === null) return json({ error: 'not_found' }, 404)\n const counts = (await submissionCounts(ctx.app, [formId])).get(formId)\n return json({ total: counts?.total ?? 0, fresh: counts?.fresh ?? 0 })\n}\n\n// ---------------------------------------------------------------------------\n// PATCH — mark new / read / archived\n// ---------------------------------------------------------------------------\n\nexport const handleSetStatus: AdminRouteHandler = async (req, ctx) => {\n const body = await readBody(req)\n if ('refusal' in body) return body.refusal\n const parsed = parseTarget(body.value, ['status'])\n const status = parsed?.rest.status\n if (parsed === null || !isSubmissionStatus(status)) {\n return json({ error: 'Expected { form: uuid, ids: uuid[], status: new|read|archived }' }, 400)\n }\n const { form } = parsed\n if ((await loadForm(ctx.app, form)) === null) return json({ error: 'not_found' }, 404)\n\n const changed = await setSubmissionStatus(ctx.app, form, parsed.ids, status)\n ctx.audit?.({\n action: 'forms.submissions.status',\n entityType: 'form',\n entityId: form,\n metadata: { status, count: changed },\n })\n return json({ changed })\n}\n\n// ---------------------------------------------------------------------------\n// DELETE — for good\n// ---------------------------------------------------------------------------\n\nexport const handleDelete: AdminRouteHandler = async (req, ctx) => {\n const body = await readBody(req)\n if ('refusal' in body) return body.refusal\n const parsed = parseTarget(body.value, [])\n if (parsed === null) return json({ error: 'Expected { form: uuid, ids: uuid[] }' }, 400)\n const { form } = parsed\n if ((await loadForm(ctx.app, form)) === null) return json({ error: 'not_found' }, 404)\n\n const deleted = await deleteSubmissions(ctx.app, form, parsed.ids)\n ctx.audit?.({\n action: 'forms.submissions.delete',\n entityType: 'form',\n entityId: form,\n metadata: { count: deleted },\n })\n return json({ deleted })\n}\n\n// ---------------------------------------------------------------------------\n// GET export — the filtered set as a CSV\n// ---------------------------------------------------------------------------\n\n/** The words a CSV needs, in the reader's interface language (English when the catalogue lacks one). */\ninterface ExportCopy {\n received: string\n page: string\n status: string\n yes: string\n no: string\n removed: string\n statuses: Record<string, string>\n}\n\nconst ENGLISH_COPY: ExportCopy = {\n received: 'Received (UTC)',\n page: 'Page',\n status: 'Status',\n yes: 'yes',\n no: 'no',\n removed: 'removed field',\n statuses: { new: 'New', read: 'Read', archived: 'Archived' },\n}\n\n/** `Forms.<...path>` in a catalogue, when it is a non-empty string. */\nfunction message(catalogue: Record<string, unknown>, path: readonly string[]): string | undefined {\n let node: unknown = catalogue.Forms\n for (const key of path) {\n if (!isRecord(node)) return undefined\n node = node[key]\n }\n return typeof node === 'string' && node !== '' ? node : undefined\n}\n\nasync function exportCopy(locale: string | undefined): Promise<ExportCopy> {\n // The loader answers English for a locale it does not ship, so a hostile or\n // unknown `uiLocale` cannot choose a file to import.\n const catalogue = await getFormsMessages(locale ?? 'en')\n const pick = (path: readonly string[], fallback: string) => message(catalogue, path) ?? fallback\n return {\n received: pick(['submissions', 'csv', 'received'], ENGLISH_COPY.received),\n page: pick(['submissions', 'csv', 'page'], ENGLISH_COPY.page),\n status: pick(['submissions', 'csv', 'status'], ENGLISH_COPY.status),\n yes: pick(['submissions', 'yes'], ENGLISH_COPY.yes),\n no: pick(['submissions', 'no'], ENGLISH_COPY.no),\n removed: pick(['submissions', 'removedField'], ENGLISH_COPY.removed),\n statuses: {\n new: pick(['submissionStatus', 'new'], 'New'),\n read: pick(['submissionStatus', 'read'], 'Read'),\n archived: pick(['submissionStatus', 'archived'], 'Archived'),\n },\n }\n}\n\n/** `2026-10-06 14:02` — a form a spreadsheet reads as a date, in UTC as the header says. */\nfunction csvTime(iso: string): string {\n return iso.slice(0, 16).replace('T', ' ')\n}\n\nfunction csvAnswer(value: SubmissionAnswer | undefined, copy: ExportCopy): string | number {\n if (value === undefined) return ''\n if (typeof value === 'boolean') return value ? copy.yes : copy.no\n return value\n}\n\nexport const handleExport: AdminRouteHandler = async (req, ctx) => {\n const url = new URL(req.url)\n const formId = url.searchParams.get('form') ?? ''\n if (!UUID_RE.test(formId)) return json({ error: 'not_found' }, 404)\n const form = await loadForm(ctx.app, formId)\n if (form === null) return json({ error: 'not_found' }, 404)\n\n // `until`: the set is read twice (below), so it is fixed at the moment the\n // export starts — a submission arriving mid-export is simply in the next one.\n // (A row stamped a moment before this instant and committed a moment after\n // the first pass can still shift one row across a page boundary: the window\n // is the length of an insert, and the cost is one repeated row.)\n const filters = {\n ...parseSubmissionFilters((name) => url.searchParams.get(name)),\n until: new Date(),\n }\n const uiLocale = url.searchParams.get('uiLocale') ?? ctx.locale\n const current = currentFormFields(form.body)\n const app = ctx.app\n const pages = (onTruncated?: (written: number) => void) =>\n pagedExportRows<SubmissionRow>(\n (offset, limit) => listSubmissionRows(app, formId, filters, { limit, offset }),\n // Exactly the reader's own page size: asking for more would be answered\n // with fewer, which reads as \"the last page\".\n { pageSize: SUBMISSIONS_READ_MAX, ...(onTruncated !== undefined && { onTruncated }) },\n )\n\n return runCsvExport(async (): Promise<CsvExport> => {\n ctx.audit?.({\n action: 'forms.submissions.export',\n entityType: 'form',\n entityId: formId,\n metadata: { filtered: submissionFilterParams(filters).size > 0 },\n })\n const copy = await exportCopy(uiLocale)\n // The header has to be written before the first row, and a retired field\n // may first appear on the last page — so the set is read once for its\n // keys, then again for its rows. Both reads are paged and capped.\n const retired = new Map<string, string>()\n for await (const row of pages()) collectRetiredColumns(current, [row], retired)\n const columns = submissionColumns(current, retired)\n\n return {\n filename: csvFilename(form.name),\n delimiter: csvDelimiterFor(uiLocale),\n header: [\n copy.received,\n copy.page,\n copy.status,\n ...columns.map((column) =>\n column.retired ? `${column.label} (${copy.removed})` : column.label,\n ),\n ],\n rows: (async function* () {\n const rows = pages((written) => {\n // The form and a count — nothing a visitor sent.\n app.logger.warn(\n { formId, written },\n 'forms: a submissions export reached the row cap — the file holds the newest rows only',\n )\n })\n for await (const row of rows) {\n yield [\n csvTime(row.createdAt),\n row.pagePath ?? '',\n copy.statuses[row.status] ?? row.status,\n ...columns.map((column) => csvAnswer(row.data[column.key], copy)),\n ]\n }\n })(),\n }\n })\n}\n\n// ---------------------------------------------------------------------------\n// Declarations\n// ---------------------------------------------------------------------------\n\nconst exportRoute = defineAdminRoute({\n prefix: SUBMISSION_ROUTES_PREFIX,\n path: 'export',\n method: 'GET',\n permission: FORM_SUBMISSIONS_VIEW,\n defaultRoles: ['admin', 'editor'],\n description: 'Download a form’s submissions as CSV: ?form=<id> plus the screen’s filters.',\n descriptionKey: 'Forms.permissions.routeExport',\n tokens: 'accept',\n handler: handleExport,\n})\n\nconst summaryRoute = defineAdminRoute({\n prefix: SUBMISSION_ROUTES_PREFIX,\n path: 'summary',\n method: 'GET',\n permission: FORM_SUBMISSIONS_VIEW,\n defaultRoles: ['admin', 'editor'],\n description: 'How many submissions a form holds and how many are new: ?form=<id>.',\n descriptionKey: 'Forms.permissions.routeSummary',\n tokens: 'accept',\n handler: handleSummary,\n})\n\nconst statusRoute = defineAdminRoute({\n prefix: SUBMISSION_ROUTES_PREFIX,\n path: '',\n method: 'PATCH',\n permission: FORM_SUBMISSIONS_MANAGE,\n defaultRoles: ['admin', 'editor'],\n description: 'Mark submissions of a form new, read or archived: { form, ids, status }.',\n descriptionKey: 'Forms.permissions.routeStatus',\n tokens: 'accept',\n handler: handleSetStatus,\n})\n\nconst deleteRoute = defineAdminRoute({\n prefix: SUBMISSION_ROUTES_PREFIX,\n path: '',\n method: 'DELETE',\n permission: FORM_SUBMISSIONS_MANAGE,\n defaultRoles: ['admin', 'editor'],\n description: 'Delete submissions of a form for good: { form, ids }.',\n descriptionKey: 'Forms.permissions.routeDelete',\n tokens: 'accept',\n // Destroys visitors' answers, up to a hundred at a time.\n confirm: true,\n handler: handleDelete,\n})\n\n/** The Submissions screen's admin routes (`server.routes`). */\nexport function submissionRoutes(): AdminRoute[] {\n return combineAdminRoutes([exportRoute, summaryRoute, statusRoute, deleteRoute])\n}\n","/**\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 routes: \"Detach a copy\", \"delete a form with its submissions\",\n * the Submissions screen's (status, delete, CSV export, summary) and\n * Privacy requests' (find by email, export, erase);\n * - the permissions — `forms:manage`, the `form` entity grants and the\n * `form_submissions` row (view / manage) — to the Permission Matrix, seeded\n * for `editor`;\n * - the catalogue (`shared.messages`);\n * - the Forms list, the form canvas, a form's Submissions and Privacy requests\n * as pages, and the sidebar link;\n * - the daily clean-up job that deletes submissions older than their form's\n * period (D007), and the table that says when it last ran.\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 {\n definePlugin,\n type PageFactory,\n type PluginNotificationTypeDefinition,\n} from '@murumets-ee/core'\nimport {\n FORM_SUBMISSIONS_VIEW,\n FORMS_MANAGE,\n Form,\n FormSubmission,\n formEmailTypes,\n formsFeatures,\n formsRoutes,\n formsSettings,\n formsTables,\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'\nimport { FormSubmissionsHeaderAction } from '@murumets-ee/forms-ui/canvas-header-action'\nimport { emailRoutes } from './email-routes.js'\nimport { privacyRoutes } from './privacy-routes.js'\nimport { submissionRoutes } from './submission-routes.js'\n\n/** Seeing the Forms list is seeing this site's forms — the `form` entity's own `view`. */\nconst FORM_LIST_GATE = `${Form.name}:view` as const\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}\nconst SubmissionsPage: PageFactory<{ locale: string; id: string }> = async (props) => {\n const mod = await import('@murumets-ee/forms-ui/pages')\n return mod.SubmissionsPage(props)\n}\n\nconst PrivacyRequestsPage: PageFactory<{ locale: string }> = async (props) => {\n const mod = await import('@murumets-ee/forms-ui/pages')\n return mod.PrivacyRequestsPage(props)\n}\n\n/** The daily clean-up of expired submissions (forms D007) — see `runFormsRetention`. */\nexport const FORMS_RETENTION_JOB = 'forms:retention'\n\n/** Nightly, off the hour so it does not start with every other 03:00 job. */\nexport const FORMS_RETENTION_SCHEDULE = '23 3 * * *'\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 // The emails are `message_template` rows sent by a queue job: the template\n // editor and the type registry are notifications', the job is the queue's.\n requires: [\n '@murumets-ee/content',\n '@murumets-ee/settings',\n '@murumets-ee/notifications',\n '@murumets-ee/queue',\n ],\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 // …and, per site, when the daily clean-up last ran and what it removed (D007).\n // One list (`formsTables`), so the erasure test walks exactly these.\n tables: { ...formsTables },\n // `POST /api/admin/form-copy` — the Form block's \"Detach a copy\" —,\n // `DELETE /api/admin/form-remove`, the Submissions screen's routes\n // (`/api/admin/form-submissions`) and Privacy requests'\n // (`/api/admin/form-privacy`).\n routes: [...formsRoutes(), ...submissionRoutes(), ...privacyRoutes(), ...emailRoutes()],\n // The daily clean-up. There is no `Plugin.server.jobs` slot to declare\n // into — a job is registered from `init`, as every plugin's is (auth's\n // approvals sweep, the queue's own trash purge); the queue materialises\n // its schedule when a worker starts. Without the queue plugin there is\n // no worker and nothing to register with: the Forms list then says the\n // clean-up has never run, rather than implying a promise is being kept.\n init: async (app) => {\n if (!app.plugins.has('@murumets-ee/queue')) return\n const { defineJob, registerJob } = await import('@murumets-ee/queue/client')\n registerJob(\n // No `description`: the queue catalogue has no translation-key slot,\n // so one would be English on every locale. What it does is above.\n defineJob({ name: FORMS_RETENTION_JOB, schedule: FORMS_RETENTION_SCHEDULE }),\n async () => {\n const { runAsCli, runInEverySite } = await import('@murumets-ee/core')\n const { runFormsRetention } = await import('@murumets-ee/forms')\n // As the synthetic `cli` admin — a job has no signed-in user — and\n // once per site: each run reads and deletes within ONE site. A\n // failure is rethrown so the queue retries and the dead-letter\n // alert fires; a promise to delete must not fail silently.\n const runs = await runAsCli(() =>\n runInEverySite(() => runFormsRetention(app), { app, label: FORMS_RETENTION_JOB }),\n )\n const removed = runs.reduce((sum, run) => sum + run.removed, 0)\n // Counts only — never what was removed (C5).\n app.logger.info(\n {\n sites: runs.length,\n removed,\n unfinished: runs.filter((run) => run.unfinished).length,\n backfilled: runs.reduce((sum, run) => sum + run.backfilled, 0),\n },\n 'forms: daily clean-up of expired submissions finished',\n )\n },\n )\n\n // forms PR05 — the email job: `forms:send_email` (see `@murumets-ee/forms/email-jobs`).\n const { runAsCli: runAsCliEmail } = await import('@murumets-ee/core')\n const { runFormEmailJob, sendFormEmailJob } = await import('@murumets-ee/forms/email-jobs')\n registerJob(sendFormEmailJob, async (job) => {\n await runAsCliEmail(async () => {\n let defaultLocale = 'en'\n try {\n const { getNotificationsConfig } = await import('@murumets-ee/notifications/plugin')\n defaultLocale = getNotificationsConfig().defaultLocale\n } catch {\n app.logger.warn(\n { outcome: 'no_default_locale' },\n 'forms: notifications default locale unavailable — using en',\n )\n }\n await runFormEmailJob(job.payload, { app, logger: app.logger, defaultLocale })\n })\n })\n },\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 // The notification and the auto-reply — `notifications()` walks this and\n // fills the type registry (and so the template editor).\n notifications: [...formEmailTypes] as unknown as PluginNotificationTypeDefinition[],\n settings: [formsSettings],\n messages: getFormsMessages,\n },\n adminUi: {\n // \"Submissions (N new)\" beside Publish on a form's canvas (PR08).\n canvasHeaderActions: { [Form.name]: FormSubmissionsHeaderAction },\n pages: {\n FormsListPage,\n FormCanvasPage,\n SubmissionsPage,\n PrivacyRequestsPage,\n },\n sidebar: [\n {\n id: 'forms',\n group: 'Content',\n label: 'Forms',\n href: '/admin/forms',\n iconName: 'clipboard-list',\n // Derived from the list page's gate in any case: whoever may see\n // this site's forms gets the link.\n requires: FORM_LIST_GATE,\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 // `form:view`, not the build job: the list is also the way in for\n // a person who only reads submissions (D005). What they can do\n // from it is decided inside the page.\n requires: FORM_LIST_GATE,\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 `form:view`.\n tokens: 'accept',\n },\n },\n {\n // Privacy requests (W5) — reached from the Forms list's header;\n // never a menu entry. A literal path: the page router matches\n // literals before `[param]` paths, so it is never read as a form id.\n path: 'forms/privacy',\n factory: 'PrivacyRequestsPage',\n page: {\n // The permission that reads submissions (forms D011): what this\n // screen finds and exports is the same answers. Erasing asks for\n // `form_submissions:manage` on its route.\n requires: FORM_SUBMISSIONS_VIEW,\n defaultRoles: ['admin', 'editor'],\n description: 'Find one person’s submissions by email address, to export or erase them.',\n descriptionKey: 'Forms.permissions.privacy',\n // Not a page about who holds authority: an agent whose role holds\n // the permission may open it to find and export, as it may a\n // form's Submissions. ERASING is closed to every agent door on\n // its route, and the page does not draw the button for one.\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 {\n // Submissions (W4) — reached from the Forms list, the form's canvas\n // and a Form block's panel; never a menu entry of its own.\n path: 'forms/[id]/submissions',\n factory: 'SubmissionsPage',\n page: {\n requires: FORM_SUBMISSIONS_VIEW,\n defaultRoles: ['admin', 'editor'],\n description: 'Read and export what visitors sent through a form.',\n descriptionKey: 'Forms.permissions.submissions',\n // An agent whose role holds the permission may open it, as it may\n // the support inbox (agent-access D021).\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":"o8CAyDA,MAAa,EAAsB,cAG7BC,EAAU,kEAEhB,SAASC,EAAK,EAAe,EAAS,IAAe,CACnD,OAAO,IAAI,SAAS,KAAK,UAAU,CAAI,EAAG,CACxC,SACA,QAAS,CAAE,eAAgB,mBAAoB,gBAAiB,UAAW,CAC7E,CAAC,CACH,CAEA,SAASC,GAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,CAAK,CAC5E,CAEA,eAAeC,EAAS,EAAmE,CACzF,IAAM,EAAO,MAAM,EAAe,EAAKJ,IAAc,EACrD,GAAI,aAAc,EAAM,MAAO,CAAE,QAASE,EAAK,CAAE,MAAO,gBAAiB,EAAG,GAAG,CAAE,EACjF,GAAI,CACF,MAAO,CAAE,MAAO,KAAK,MAAM,EAAK,IAAI,CAAa,CACnD,MAAQ,CACN,MAAO,CAAE,QAASA,EAAK,CAAE,MAAO,cAAe,EAAG,GAAG,CAAE,CACzD,CACF,CAGA,SAAS,EAAO,EAAgB,EAAyD,CACvF,GAAI,CAACC,GAAS,CAAK,EAAG,OAAO,KAC7B,IAAM,EAAU,IAAI,IAAI,CAAI,EAC5B,OAAO,OAAO,KAAK,CAAK,CAAC,CAAC,MAAO,GAAQ,EAAQ,IAAI,CAAG,CAAC,EAAI,EAAQ,IACvE,CAGA,eAAeE,EAAS,EAAiB,EAA+C,CACtF,IAAM,EAAkD,MAAM,EAC3D,gBAAgB,CAAI,CAAC,CACrB,SAAS,CAAE,EACd,OAAO,GAAQ,KAA4B,KAAO,CAAE,KAAM,EAAI,IAAK,CACrE,CAEA,MAAa,GAAsC,MAAO,EAAK,IAAQ,CACrE,IAAM,EAAO,MAAMD,EAAS,CAAG,EAC/B,GAAI,YAAa,EAAM,OAAO,EAAK,QACnC,IAAM,EAAS,EAAO,EAAK,MAAO,CAAC,OAAQ,aAAc,mBAAoB,gBAAgB,CAAC,EACxF,CAAE,OAAM,aAAY,mBAAkB,kBAAmB,GAAU,CAAC,EAC1E,GACE,IAAW,MACX,OAAO,GAAS,UAChB,CAACH,EAAQ,KAAK,CAAI,GAClB,OAAO,GAAqB,WAC5B,EAAE,IAAmB,MAAQ,OAAO,GAAmB,UAEvD,OAAOC,EACL,CACE,MACE,uGACJ,EACA,GACF,EAEF,IAAM,EAAU,GAAgB,CAAU,EAC1C,GAAI,CAAC,EAAQ,GACX,OAAOA,EACL,CAAE,MAAO,qBAAsB,QAAS,EAAQ,QAAS,QAAS,EAAQ,OAAQ,EAClF,GACF,EAEF,IAAM,EAAS,MAAMG,EAAS,EAAI,IAAK,CAAI,EAC3C,GAAI,IAAW,KAAM,OAAOH,EAAK,CAAE,MAAO,WAAY,EAAG,GAAG,EAI5D,IAAM,GADS,EAAY,EAAO,IAAI,CAAC,EAAE,QAAU,CAAC,EAAA,CAC3B,OAAQ,GAAU,EAAM,OAAS,OAAO,CAAC,CAAC,IAAK,GAAU,EAAM,GAAG,EAmB3F,OAlBI,IAAmB,MAAQ,CAAC,EAAU,SAAS,CAAc,EACxDA,EAAK,CAAE,MAAO,0BAA2B,EAAG,GAAG,EAEpD,GAAoB,IAAmB,KAClCA,EAAK,CAAE,MAAO,wBAAyB,EAAG,GAAG,GAGtD,MAAM,GAAmB,EAAI,IAAK,EAAM,CACtC,WAAY,EAAQ,WACpB,mBACA,gBACF,CAAC,EACD,EAAI,QAAQ,CACV,OAAQ,sBACR,WAAY,OACZ,SAAU,EACV,SAAU,CAAE,WAAY,EAAQ,WAAW,OAAQ,UAAW,CAAiB,CACjF,CAAC,EACMA,EAAK,CAAE,WAAY,EAAQ,WAAY,mBAAkB,gBAAe,CAAC,EAClF,EAOM,GAAqC,CAAC,gBAAiB,UAAU,EAGvE,SAAS,GACP,EACA,EACS,CAWT,OAVI,EAAI,SAAW,SAOV,EAHL,EAAI,OAAS,aACb,EAAI,aAAe,WACnB,EAAM,IAAI,KAAK,EAAI,SAAS,CAAC,CAAC,QAAQ,EAAI,MAG1C,EAAI,SAAW,SAAiB,EAAM,IAAI,KAAK,EAAI,SAAS,CAAC,CAAC,QAAQ,EAAI,IAE5E,EAAI,SAAW,WAAa,EAAI,aAAe,MAAQ,GAAgB,SAAS,EAAI,UAAU,CAElG,CAEA,MAAa,GAAiC,MAAO,EAAK,IAAQ,CAChE,IAAM,EAAO,MAAME,EAAS,CAAG,EAC/B,GAAI,YAAa,EAAM,OAAO,EAAK,QACnC,IAAM,EAAS,EAAO,EAAK,MAAO,CAAC,OAAQ,aAAc,MAAM,CAAC,EAC1D,CAAE,OAAM,aAAY,QAAS,GAAU,CAAC,EAC9C,GACE,IAAW,MACX,OAAO,GAAS,UAChB,CAACH,EAAQ,KAAK,CAAI,GAClB,OAAO,GAAe,UACtB,CAACA,EAAQ,KAAK,CAAU,GACxB,CAAC,GAAY,CAAI,EAEjB,OAAOC,EACL,CAAE,MAAO,yEAA0E,EACnF,GACF,EAEF,GAAK,MAAMG,EAAS,EAAI,IAAK,CAAI,IAAO,KAAM,OAAOH,EAAK,CAAE,MAAO,WAAY,EAAG,GAAG,EAGrF,IAAM,EAAkD,MAAM,EAAI,IAC/D,gBAAgB,CAAc,CAAC,CAC/B,SAAS,CAAU,EACtB,GAAI,GAAQ,MAA6B,OAAO,EAAI,IAAI,IAAM,EAC5D,OAAOA,EAAK,CAAE,MAAO,WAAY,EAAG,GAAG,EAMzC,IAAM,IADW,MAAM,GAAe,EAAI,IAAK,EAAM,CAAC,CAAU,CAAC,EAAA,CAAG,IAAI,CAAU,GAAK,CAAC,EAAA,CAC/D,KAAM,GAAU,EAAM,OAAS,CAAI,EAC5D,GAAI,IAAa,IAAA,IAAa,CAAC,GAAY,EAAU,KAAK,IAAI,CAAC,EAC7D,OAAOA,EAAK,CAAE,MAAO,gBAAiB,OAAQ,EAAS,MAAO,EAAG,GAAG,EAEtE,IAAM,EAAO,MAAM,EAAmB,EAA0B,EAAI,GAAG,EAEjE,EAAS,MAAM,GAAiB,EAAI,IAAK,CAC7C,OACA,OAAQ,EACR,aAAc,EACd,MACF,CAAC,EASD,OARA,EAAI,QAAQ,CACV,OAAQ,qBACR,WAAY,EACZ,SAAU,EACV,SAAU,CAAE,OAAM,QAAO,CAC3B,CAAC,EAGMA,EAAK,CAAE,QAAO,CAAC,CACxB,EAEM,GAAY,EAAiB,CACjC,OAAQ,EACR,KAAM,GACN,OAAQ,QACR,WAAY,EACZ,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,yEACb,eAAgB,gCAChB,OAAQ,SAGR,QAAS,GACT,QAAS,EACX,CAAC,EAEK,GAAa,EAAiB,CAClC,OAAQ,EACR,KAAM,QACN,OAAQ,OACR,WAAY,EACZ,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,+DACb,eAAgB,oCAChB,OAAQ,SACR,QAAS,EACX,CAAC,EAGD,SAAgB,IAA4B,CAC1C,OAAO,EAAmB,CAAC,GAAW,EAAU,CAAC,CACnD,CCtLA,MAAa,EAAwB,eAGxB,EAAmB,0BAIhC,IAAI,EAAU,EAOd,SAAS,IAAiB,CACxB,OAAO,IAAI,SAAS,KAAK,UAAU,CAAE,MAAO,cAAe,KAAM,aAAc,CAAC,EAAG,CACjF,OAAQ,IACR,QAAS,CACP,eAAgB,mBAChB,gBAAiB,WACjB,cAAe,IACjB,CACF,CAAC,CACH,CAKA,SAASK,EAAK,EAAe,EAAS,IAAe,CACnD,OAAO,IAAI,SAAS,KAAK,UAAU,CAAI,EAAG,CACxC,SACA,QAAS,CAAE,eAAgB,mBAAoB,gBAAiB,UAAW,CAC7E,CAAC,CACH,CAEA,SAASC,EAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,CAAK,CAC5E,CAMA,eAAe,EACb,EACA,EACmF,CAEnF,IAAM,EAAO,MAAM,EAAe,EAAKF,IAAc,EACrD,GAAI,aAAc,EAAM,MAAO,CAAE,QAASC,EAAK,CAAE,MAAO,gBAAiB,EAAG,GAAG,CAAE,EACjF,IAAI,EACJ,GAAI,CACF,EAAQ,KAAK,MAAM,EAAK,IAAI,CAC9B,MAAQ,CACN,MAAO,CAAE,QAASA,EAAK,CAAE,MAAO,cAAe,EAAG,GAAG,CAAE,CACzD,CACA,IAAM,EAAU,IAAI,IAAI,CAAC,QAAS,GAAG,CAAS,CAAC,EAC/C,GAAI,CAACC,EAAS,CAAK,GAAK,OAAO,KAAK,CAAK,CAAC,CAAC,KAAM,GAAQ,CAAC,EAAQ,IAAI,CAAG,CAAC,EACxE,MAAO,CAAE,QAASD,EAAK,CAAE,MAAO,cAAe,EAAG,GAAG,CAAE,EAEzD,IAAM,EAAQ,GAAkB,EAAM,KAAK,EAI3C,OAFI,IAAU,KACL,CAAE,QAASA,EAAK,CAAE,MAAO,gBAAiB,KAAM,eAAgB,EAAG,GAAG,CAAE,EAC1E,CAAE,QAAO,KAAM,CAAM,CAC9B,CAMA,MAAa,GAAyC,MAAO,EAAK,IAAQ,CACxE,IAAM,EAAU,MAAM,EAAY,EAAK,CAAC,CAAC,EACzC,GAAI,YAAa,EAAS,OAAO,EAAQ,QACzC,GAAM,CAAE,SAAU,EACZ,CAAC,EAAO,EAAM,GAAa,MAAM,QAAQ,IAAI,CACjD,GAAwB,EAAI,IAAK,CAAK,EACtC,EAAuB,EAAI,IAAK,EAAO,CAAE,MAAO,EAAkB,OAAQ,CAAE,CAAC,EAC7E,EAA0B,EAAI,GAAG,CACnC,CAAC,EACK,EAAQ,MAAM,EAClB,EAAI,IACJ,EAAK,IAAK,GAAQ,EAAI,MAAM,CAC9B,EACM,EAAwB,EAAK,IAAK,IAAS,CAC/C,GAAI,EAAI,GACR,OAAQ,EAAI,OACZ,SAAU,EAAM,IAAI,EAAI,MAAM,GAAK,KACnC,UAAW,EAAI,UACf,SAAU,EAAI,QAChB,EAAE,EAMF,OALA,EAAI,QAAQ,CACV,OAAQ,uBACR,WAAY,kBACZ,SAAU,CAAE,MAAO,EAAO,MAAO,CAAC,GAAG,IAAI,IAAI,EAAK,IAAK,GAAQ,EAAI,MAAM,CAAC,CAAC,EAAG,WAAU,CAC1F,CAAC,EACMA,EAAK,CAAE,QAAO,QAAO,YAAW,UAAW,CAAmB,CAAC,CACxE,EAMM,GAAU,CAAC,MAAO,MAAM,EAe9B,SAASE,GAAQ,EAAoC,EAA6C,CAChG,IAAI,EAAgB,EAAU,MAC9B,IAAK,IAAM,KAAO,EAAM,CACtB,GAAI,CAACD,EAAS,CAAI,EAAG,OACrB,EAAO,EAAK,EACd,CACA,OAAO,OAAO,GAAS,UAAY,IAAS,GAAK,EAAO,IAAA,EAC1D,CAEA,eAAeE,GAAW,EAAiD,CAGzE,IAAM,EAAY,MAAM,EAAiB,GAAU,IAAI,EACjD,GAAQ,EAAyB,IAAqBD,GAAQ,EAAW,CAAI,GAAK,EACxF,MAAO,CACL,KAAM,EAAK,CAAC,UAAW,MAAO,MAAM,EAAG,MAAM,EAC7C,SAAU,EAAK,CAAC,UAAW,MAAO,UAAU,EAAG,gBAAgB,EAC/D,KAAM,EAAK,CAAC,UAAW,MAAO,MAAM,EAAG,MAAM,EAC7C,OAAQ,EAAK,CAAC,UAAW,MAAO,QAAQ,EAAG,QAAQ,EACnD,IAAK,EAAK,CAAC,cAAe,KAAK,EAAG,KAAK,EACvC,GAAI,EAAK,CAAC,cAAe,IAAI,EAAG,IAAI,EACpC,YAAa,EAAK,CAAC,UAAW,aAAa,EAAG,SAAS,EACvD,SAAU,CACR,IAAK,EAAK,CAAC,mBAAoB,KAAK,EAAG,KAAK,EAC5C,KAAM,EAAK,CAAC,mBAAoB,MAAM,EAAG,MAAM,EAC/C,SAAU,EAAK,CAAC,mBAAoB,UAAU,EAAG,UAAU,CAC7D,CACF,CACF,CAGA,SAAS,EAAQ,EAAuB,EAAqB,CAC3D,IAAM,EAAQ,EAAI,OAAO,EAAI,EAAE,MAC/B,OAAO,IAAU,IAAA,IAAa,IAAU,GAAK,EAAQ,CACvD,CAGA,SAAS,GAAe,EAAwD,CAC9E,IAAM,EAAU,IAAI,IACpB,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,EAAI,IAAI,EAAG,CACnD,IAAM,EAAQ,EAAQ,EAAK,CAAG,EAC9B,EAAQ,IAAI,EAAO,CAAC,GAAI,EAAQ,IAAI,CAAK,GAAK,CAAC,EAAI,CAAK,CAAC,CAC3D,CACA,OAAO,CACT,CAEA,SAAS,GAAK,EAAiD,EAAmC,CAChG,GAAI,IAAW,IAAA,IAAa,EAAO,SAAW,EAAG,MAAO,GACxD,IAAM,EAAQ,EAAO,IAAK,GACxB,OAAO,GAAU,UAAa,EAAQ,EAAK,IAAM,EAAK,GAAM,CAC9D,EACM,EAAO,EAAM,GACnB,OAAO,EAAM,SAAW,GAAK,IAAS,IAAA,GAAY,EAAO,EAAM,KAAK,KAAK,CAC3E,CAGA,SAASE,GAAQ,EAAqB,CACpC,OAAO,EAAI,MAAM,EAAG,EAAE,CAAC,CAAC,QAAQ,IAAK,GAAG,CAC1C,CAGA,SAAS,GACP,EACA,EACA,EACA,EACmD,CACnD,OAAO,GACJ,EAAQ,IAAU,EAAuB,EAAK,EAAO,CAAE,QAAO,SAAQ,OAAM,CAAC,EAC9E,CACE,SAAU,EACV,QAAS,EACT,GAAI,IAAgB,IAAA,IAAa,CAAE,aAAY,CACjD,CACF,CACF,CAEA,MAAa,GAAyC,MAAO,EAAK,IAAQ,CACxE,IAAM,EAAU,MAAM,EAAY,EAAK,CAAC,SAAU,UAAU,CAAC,EAC7D,GAAI,YAAa,EAAS,OAAO,EAAQ,QACzC,GAAM,CAAE,QAAO,QAAS,EAClB,EAAmC,GAAQ,KAAM,GAAS,IAAS,EAAK,MAAM,EACpF,GAAI,IAAW,IAAA,GAAW,OAAOJ,EAAK,CAAE,MAAO,gBAAiB,EAAG,GAAG,EACtE,IAAM,EACJ,OAAO,EAAK,UAAa,UAAY,EAAK,SAAS,QAAU,GAAK,EAAK,SAAW,EAAI,OAClF,EAAM,EAAI,IAOhB,GAAI,GAAW,EAAkB,OAAO,GAAK,EAC7C,GAAW,EACX,IAAM,EAAQ,IAAI,KACZ,EAA2B,CAAC,EAC9B,EAAY,GACZ,EACJ,GAAI,CACF,UAAW,IAAM,KAAO,GAAW,EAAK,EAAO,MAAa,CAC1D,EAAY,EACd,CAAC,EACC,EAAK,KAAK,CAAG,EAEf,EAAQ,MAAM,EAAU,EAAK,CAAC,GAAG,IAAI,IAAI,EAAK,IAAK,GAAQ,EAAI,MAAM,CAAC,CAAC,CAAC,CAC1E,QAAU,CACR,GACF,CACI,GAEF,EAAI,OAAO,KACT,CAAE,QAAS,EAAK,MAAO,EACvB,mFACF,EAIF,IAAM,MAAoB,CACxB,EAAI,QAAQ,CACV,OAAQ,uBACR,WAAY,kBACZ,SAAU,CACR,MAAO,EAAK,OACZ,MAAO,CAAC,GAAG,IAAI,IAAI,EAAK,IAAK,GAAQ,EAAI,MAAM,CAAC,CAAC,EACjD,SACA,WACF,CACF,CAAC,CACH,EAEA,GAAI,IAAW,OAAQ,CACrB,IAAM,EAAO,KAAK,UAChB,CACE,WAAY,EAAM,YAAY,EAC9B,YACA,YAAa,EAAK,IAAK,IAAS,CAC9B,KAAM,EAAM,IAAI,EAAI,MAAM,GAAK,KAC/B,WAAY,EAAI,UAChB,KAAM,EAAI,SACV,SAAU,EAAI,OACd,OAAQ,EAAI,OACZ,QAAS,OAAO,QAAQ,EAAI,IAAI,CAAC,CAAC,KAAK,CAAC,EAAK,MAAY,CACvD,MAAO,EAAQ,EAAK,CAAG,EACvB,MACA,OAAQ,CACV,EAAE,EACF,QAAS,OAAO,OAAO,EAAI,OAAO,CACpC,EAAE,CACJ,EACA,KACA,CACF,EAEA,OADA,EAAM,EACC,IAAI,SAAS,EAAM,CACxB,OAAQ,IACR,QAAS,CACP,eAAgB,kCAChB,sBAAuB,yBAAyB,EAAY,iBAAiB,CAAC,CAAC,QAAQ,SAAU,OAAO,EAAE,GAC1G,gBAAiB,oBACjB,yBAA0B,WACzB,GAAmB,OAAO,CAAS,CACtC,CACF,CAAC,CACH,CAEA,IAAM,EAAO,MAAMG,GAAW,CAAQ,EAChC,EAAmB,CAAC,EACpB,EAAO,IAAI,IACjB,IAAK,IAAM,KAAO,EAChB,IAAK,IAAM,KAAO,OAAO,KAAK,EAAI,IAAI,EAAG,CACvC,IAAM,EAAQ,EAAQ,EAAK,CAAG,EAC1B,EAAK,IAAI,CAAK,IAClB,EAAK,IAAI,CAAK,EACd,EAAO,KAAK,CAAK,EACnB,CAEF,IAAM,EAAM,MAAM,OACE,CAChB,SAAU,EAAY,iBAAiB,EACvC,UAAW,EAAgB,CAAQ,EACnC,OAAQ,CAAC,EAAK,KAAM,EAAK,SAAU,EAAK,KAAM,EAAK,OAAQ,GAAG,CAAM,EACpE,KAAM,EAAK,IAAK,GAAQ,CACtB,IAAM,EAAU,GAAe,CAAG,EAClC,MAAO,CACL,EAAM,IAAI,EAAI,MAAM,GAAK,EAAK,YAC9BC,GAAQ,EAAI,SAAS,EACrB,EAAI,UAAY,GAChB,EAAK,SAAS,EAAI,SAAW,EAAI,OACjC,GAAG,EAAO,IAAK,GAAU,GAAK,EAAQ,IAAI,CAAK,EAAG,CAAI,CAAC,CACzD,CACF,CAAC,CACH,EACF,EAKA,OAJI,EAAI,KACN,EAAM,EACN,EAAI,QAAQ,IAAI,EAAkB,OAAO,CAAS,CAAC,GAE9C,CACT,EAMa,GAAwC,MAAO,EAAK,IAAQ,CACvE,IAAM,EAAU,MAAM,EAAY,EAAK,CAAC,CAAC,EACzC,GAAI,YAAa,EAAS,OAAO,EAAQ,QAGzC,IAAM,EAAS,MAAM,GAAwB,EAAI,IAAK,EAAQ,KAAK,EAC7D,EAAY,MAAM,EAA0B,EAAI,GAAG,CAAC,CAAC,UAAY,IAAI,EAa3E,OAZA,EAAI,QAAQ,CACV,OAAQ,sBACR,WAAY,kBAEZ,SAAU,CACR,MAAO,EAAO,OACd,MAAO,CAAC,GAAG,EAAO,OAAO,EACzB,WAAY,EAAO,KACnB,OAAQ,EAAO,OACf,WACF,CACF,CAAC,EACMJ,EACL,CACE,OAAQ,EAAO,OACf,MAAO,EAAO,QAAQ,OACtB,KAAM,EAAO,KACb,OAAQ,EAAO,OACf,WACF,EACA,EAAO,OAAS,IAAM,GACxB,CACF,EAMM,GAAc,EAAiB,CACnC,OAAQ,EACR,KAAM,SACN,OAAQ,OACR,WAAY,EACZ,aAAc,CAAC,QAAS,QAAQ,EAEhC,YAAa,sEACb,eAAgB,uCAChB,OAAQ,SACR,QAAS,EACX,CAAC,EAEKK,GAAc,EAAiB,CACnC,OAAQ,EACR,KAAM,SACN,OAAQ,OACR,WAAY,EACZ,aAAc,CAAC,QAAS,QAAQ,EAEhC,YAAa,uEACb,eAAgB,uCAChB,OAAQ,SACR,QAAS,EACX,CAAC,EAEK,GAAa,EAAiB,CAClC,OAAQ,EACR,KAAM,GACN,OAAQ,SACR,WAAY,EACZ,aAAc,CAAC,QAAS,QAAQ,EAEhC,YAAa,8EACb,eAAgB,sCAIhB,OAAQ,CAAE,OAAQ,eAAgB,EAClC,QAAS,EACX,CAAC,EAGD,SAAgB,IAA8B,CAC5C,OAAO,EAAmB,CAAC,GAAaA,GAAa,EAAU,CAAC,CAClE,CC1ZA,MAAa,EAA2B,mBAKxC,SAAS,EAAK,EAAe,EAAS,IAAe,CACnD,OAAO,IAAI,SAAS,KAAK,UAAU,CAAI,EAAG,CACxC,SACA,QAAS,CAAE,eAAgB,mBAAoB,gBAAiB,UAAW,CAC7E,CAAC,CACH,CAEA,MAAM,EAAU,kEAEhB,SAAS,EAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,CAAK,CAC5E,CAGA,SAAS,GAAe,EAAoC,CAC1D,OAAO,MAAM,QAAQ,CAAK,CAC5B,CAMA,SAAS,EACP,EACA,EACuE,CACvE,GAAI,CAAC,EAAS,CAAK,EAAG,OAAO,KAC7B,IAAM,EAAU,IAAI,IAAI,CAAC,OAAQ,MAAO,GAAG,CAAS,CAAC,EACrD,GAAI,OAAO,KAAK,CAAK,CAAC,CAAC,KAAM,GAAQ,CAAC,EAAQ,IAAI,CAAG,CAAC,EAAG,OAAO,KAChE,GAAM,CAAE,OAAM,OAAQ,EAEtB,GADI,OAAO,GAAS,UAAY,CAAC,EAAQ,KAAK,CAAI,GAC9C,CAAC,GAAe,CAAG,GAAK,EAAI,SAAW,GAAK,EAAI,OAAS,EAAqB,OAAO,KACzF,IAAM,EAAQ,EAAI,OAAQ,GAAqB,OAAO,GAAO,UAAY,EAAQ,KAAK,CAAE,CAAC,EAEzF,OADI,EAAM,SAAW,EAAI,OAClB,CAAE,OAAM,IAAK,EAAO,KAAM,CAAM,EADC,IAE1C,CAGA,eAAe,EAAS,EAAmE,CAEzF,IAAM,EAAO,MAAM,EAAe,EAAK,IAAc,EACrD,GAAI,aAAc,EAAM,MAAO,CAAE,QAAS,EAAK,CAAE,MAAO,gBAAiB,EAAG,GAAG,CAAE,EACjF,IAAM,EAAM,EAAK,KACjB,GAAI,CAEF,MAAO,CAAE,MADc,KAAK,MAAM,CACrB,CAAE,CACjB,MAAQ,CACN,MAAO,CAAE,QAAS,EAAK,CAAE,MAAO,cAAe,EAAG,GAAG,CAAE,CACzD,CACF,CAQA,eAAe,EACb,EACA,EACiD,CACjD,IAAM,EAAkD,MAAM,EAC3D,gBAAgB,CAAI,CAAC,CACrB,SAAS,CAAE,EAEd,OADI,GAAQ,KAAkC,KACvC,CAAE,KAAM,OAAO,EAAI,MAAS,SAAW,EAAI,KAAO,OAAQ,KAAM,EAAI,IAAK,CAClF,CAMA,MAAa,GAAmC,MAAO,EAAK,IAAQ,CAClE,IAAM,EAAS,IAAI,IAAI,EAAI,GAAG,CAAC,CAAC,aAAa,IAAI,MAAM,GAAK,GAE5D,GADI,CAAC,EAAQ,KAAK,CAAM,GACnB,MAAM,EAAS,EAAI,IAAK,CAAM,IAAO,KAAM,OAAO,EAAK,CAAE,MAAO,WAAY,EAAG,GAAG,EACvF,IAAM,GAAU,MAAM,GAAiB,EAAI,IAAK,CAAC,CAAM,CAAC,EAAA,CAAG,IAAI,CAAM,EACrE,OAAO,EAAK,CAAE,MAAO,GAAQ,OAAS,EAAG,MAAO,GAAQ,OAAS,CAAE,CAAC,CACtE,EAMa,GAAqC,MAAO,EAAK,IAAQ,CACpE,IAAM,EAAO,MAAM,EAAS,CAAG,EAC/B,GAAI,YAAa,EAAM,OAAO,EAAK,QACnC,IAAM,EAAS,EAAY,EAAK,MAAO,CAAC,QAAQ,CAAC,EAC3C,EAAS,GAAQ,KAAK,OAC5B,GAAI,IAAW,MAAQ,CAAC,GAAmB,CAAM,EAC/C,OAAO,EAAK,CAAE,MAAO,iEAAkE,EAAG,GAAG,EAE/F,GAAM,CAAE,QAAS,EACjB,GAAK,MAAM,EAAS,EAAI,IAAK,CAAI,IAAO,KAAM,OAAO,EAAK,CAAE,MAAO,WAAY,EAAG,GAAG,EAErF,IAAM,EAAU,MAAM,GAAoB,EAAI,IAAK,EAAM,EAAO,IAAK,CAAM,EAO3E,OANA,EAAI,QAAQ,CACV,OAAQ,2BACR,WAAY,OACZ,SAAU,EACV,SAAU,CAAE,SAAQ,MAAO,CAAQ,CACrC,CAAC,EACM,EAAK,CAAE,SAAQ,CAAC,CACzB,EAMa,GAAkC,MAAO,EAAK,IAAQ,CACjE,IAAM,EAAO,MAAM,EAAS,CAAG,EAC/B,GAAI,YAAa,EAAM,OAAO,EAAK,QACnC,IAAM,EAAS,EAAY,EAAK,MAAO,CAAC,CAAC,EACzC,GAAI,IAAW,KAAM,OAAO,EAAK,CAAE,MAAO,sCAAuC,EAAG,GAAG,EACvF,GAAM,CAAE,QAAS,EACjB,GAAK,MAAM,EAAS,EAAI,IAAK,CAAI,IAAO,KAAM,OAAO,EAAK,CAAE,MAAO,WAAY,EAAG,GAAG,EAErF,IAAM,EAAU,MAAM,GAAkB,EAAI,IAAK,EAAM,EAAO,GAAG,EAOjE,OANA,EAAI,QAAQ,CACV,OAAQ,2BACR,WAAY,OACZ,SAAU,EACV,SAAU,CAAE,MAAO,CAAQ,CAC7B,CAAC,EACM,EAAK,CAAE,SAAQ,CAAC,CACzB,EAiBM,EAA2B,CAC/B,SAAU,iBACV,KAAM,OACN,OAAQ,SACR,IAAK,MACL,GAAI,KACJ,QAAS,gBACT,SAAU,CAAE,IAAK,MAAO,KAAM,OAAQ,SAAU,UAAW,CAC7D,EAGA,SAAS,GAAQ,EAAoC,EAA6C,CAChG,IAAI,EAAgB,EAAU,MAC9B,IAAK,IAAM,KAAO,EAAM,CACtB,GAAI,CAAC,EAAS,CAAI,EAAG,OACrB,EAAO,EAAK,EACd,CACA,OAAO,OAAO,GAAS,UAAY,IAAS,GAAK,EAAO,IAAA,EAC1D,CAEA,eAAe,EAAW,EAAiD,CAGzE,IAAM,EAAY,MAAM,EAAiB,GAAU,IAAI,EACjD,GAAQ,EAAyB,IAAqB,GAAQ,EAAW,CAAI,GAAK,EACxF,MAAO,CACL,SAAU,EAAK,CAAC,cAAe,MAAO,UAAU,EAAG,EAAa,QAAQ,EACxE,KAAM,EAAK,CAAC,cAAe,MAAO,MAAM,EAAG,EAAa,IAAI,EAC5D,OAAQ,EAAK,CAAC,cAAe,MAAO,QAAQ,EAAG,EAAa,MAAM,EAClE,IAAK,EAAK,CAAC,cAAe,KAAK,EAAG,EAAa,GAAG,EAClD,GAAI,EAAK,CAAC,cAAe,IAAI,EAAG,EAAa,EAAE,EAC/C,QAAS,EAAK,CAAC,cAAe,cAAc,EAAG,EAAa,OAAO,EACnE,SAAU,CACR,IAAK,EAAK,CAAC,mBAAoB,KAAK,EAAG,KAAK,EAC5C,KAAM,EAAK,CAAC,mBAAoB,MAAM,EAAG,MAAM,EAC/C,SAAU,EAAK,CAAC,mBAAoB,UAAU,EAAG,UAAU,CAC7D,CACF,CACF,CAGA,SAAS,GAAQ,EAAqB,CACpC,OAAO,EAAI,MAAM,EAAG,EAAE,CAAC,CAAC,QAAQ,IAAK,GAAG,CAC1C,CAEA,SAAS,GAAU,EAAqC,EAAmC,CAGzF,OAFI,IAAU,IAAA,GAAkB,GAC5B,OAAO,GAAU,UAAkB,EAAQ,EAAK,IAAM,EAAK,GACxD,CACT,CAgFA,MAAM,GAAc,EAAiB,CACnC,OAAQ,EACR,KAAM,SACN,OAAQ,MACR,WAAY,EACZ,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,8EACb,eAAgB,gCAChB,OAAQ,SACR,QAAS,MAvF2C,EAAK,IAAQ,CACjE,IAAM,EAAM,IAAI,IAAI,EAAI,GAAG,EACrB,EAAS,EAAI,aAAa,IAAI,MAAM,GAAK,GAC/C,GAAI,CAAC,EAAQ,KAAK,CAAM,EAAG,OAAO,EAAK,CAAE,MAAO,WAAY,EAAG,GAAG,EAClE,IAAM,EAAO,MAAM,EAAS,EAAI,IAAK,CAAM,EAC3C,GAAI,IAAS,KAAM,OAAO,EAAK,CAAE,MAAO,WAAY,EAAG,GAAG,EAO1D,IAAM,EAAU,CACd,GAAG,GAAwB,GAAS,EAAI,aAAa,IAAI,CAAI,CAAC,EAC9D,MAAO,IAAI,IACb,EACM,EAAW,EAAI,aAAa,IAAI,UAAU,GAAK,EAAI,OACnD,EAAU,EAAkB,EAAK,IAAI,EACrC,EAAM,EAAI,IACV,EAAS,GACb,GACG,EAAQ,IAAU,GAAmB,EAAK,EAAQ,EAAS,CAAE,QAAO,QAAO,CAAC,EAG7E,CAAE,SAAU,EAAsB,GAAI,IAAgB,IAAA,IAAa,CAAE,aAAY,CAAG,CACtF,EAEF,OAAO,EAAa,SAAgC,CAClD,EAAI,QAAQ,CACV,OAAQ,2BACR,WAAY,OACZ,SAAU,EACV,SAAU,CAAE,SAAU,GAAuB,CAAO,CAAC,CAAC,KAAO,CAAE,CACjE,CAAC,EACD,IAAM,EAAO,MAAM,EAAW,CAAQ,EAIhC,EAAU,IAAI,IACpB,UAAW,IAAM,KAAO,EAAM,EAAG,EAAsB,EAAS,CAAC,CAAG,EAAG,CAAO,EAC9E,IAAM,EAAU,GAAkB,EAAS,CAAO,EAElD,MAAO,CACL,SAAU,EAAY,EAAK,IAAI,EAC/B,UAAW,EAAgB,CAAQ,EACnC,OAAQ,CACN,EAAK,SACL,EAAK,KACL,EAAK,OACL,GAAG,EAAQ,IAAK,GACd,EAAO,QAAU,GAAG,EAAO,MAAM,IAAI,EAAK,QAAQ,GAAK,EAAO,KAChE,CACF,EACA,MAAO,iBAAmB,CACxB,IAAM,EAAO,EAAO,GAAY,CAE9B,EAAI,OAAO,KACT,CAAE,SAAQ,SAAQ,EAClB,uFACF,CACF,CAAC,EACD,UAAW,IAAM,KAAO,EACtB,KAAM,CACJ,GAAQ,EAAI,SAAS,EACrB,EAAI,UAAY,GAChB,EAAK,SAAS,EAAI,SAAW,EAAI,OACjC,GAAG,EAAQ,IAAK,GAAW,GAAU,EAAI,KAAK,EAAO,KAAM,CAAI,CAAC,CAClE,CAEJ,EAAA,CAAG,CACL,CACF,CAAC,CACH,CAgBA,CAAC,EAEK,GAAe,EAAiB,CACpC,OAAQ,EACR,KAAM,UACN,OAAQ,MACR,WAAY,EACZ,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,sEACb,eAAgB,iCAChB,OAAQ,SACR,QAAS,EACX,CAAC,EAEK,GAAc,EAAiB,CACnC,OAAQ,EACR,KAAM,GACN,OAAQ,QACR,WAAY,EACZ,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,2EACb,eAAgB,gCAChB,OAAQ,SACR,QAAS,EACX,CAAC,EAEK,GAAc,EAAiB,CACnC,OAAQ,EACR,KAAM,GACN,OAAQ,SACR,WAAY,EACZ,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,wDACb,eAAgB,gCAChB,OAAQ,SAER,QAAS,GACT,QAAS,EACX,CAAC,EAGD,SAAgB,IAAiC,CAC/C,OAAO,EAAmB,CAAC,GAAa,GAAc,GAAa,EAAW,CAAC,CACjF,CClVA,MAAM,EAAiB,GAAG,EAAK,KAAK,OAE9B,GAAiD,KAAO,KAErD,MADW,OAAO,+BAAA,CACd,cAAc,CAAK,EAE1B,GAA8D,KAAO,KAElE,MADW,OAAO,+BAAA,CACd,eAAe,CAAK,EAE3B,GAA+D,KAAO,KAEnE,MADW,OAAO,+BAAA,CACd,gBAAgB,CAAK,EAG5B,GAAuD,KAAO,KAE3D,MADW,OAAO,+BAAA,CACd,oBAAoB,CAAK,EAIzB,EAAsB,kBAGtB,EAA2B,aAExC,SAAgB,IAAc,CAC5B,OAAO,EAAa,CAClB,KAAM,qBAKN,SAAU,CACR,uBACA,wBACA,6BACA,oBACF,EACA,OAAQ,CAIN,SAAU,CAAC,EAAM,CAAc,EAI/B,OAAQ,CAAE,GAAG,EAAY,EAKzB,OAAQ,CAAC,GAAG,EAAY,EAAG,GAAG,GAAiB,EAAG,GAAG,GAAc,EAAG,GAAG,GAAY,CAAC,EAOtF,KAAM,KAAO,IAAQ,CACnB,GAAI,CAAC,EAAI,QAAQ,IAAI,oBAAoB,EAAG,OAC5C,GAAM,CAAE,YAAW,eAAgB,MAAM,OAAO,6BAChD,EAGE,EAAU,CAAE,KAAM,EAAqB,SAAU,CAAyB,CAAC,EAC3E,SAAY,CACV,GAAM,CAAE,WAAU,kBAAmB,MAAM,OAAO,qBAC5C,CAAE,qBAAsB,MAAM,OAAO,sBAKrC,EAAO,MAAM,MACjB,MAAqB,EAAkB,CAAG,EAAG,CAAE,MAAK,MAAO,CAAoB,CAAC,CAClF,EACM,EAAU,EAAK,QAAQ,EAAK,IAAQ,EAAM,EAAI,QAAS,CAAC,EAE9D,EAAI,OAAO,KACT,CACE,MAAO,EAAK,OACZ,UACA,WAAY,EAAK,OAAQ,GAAQ,EAAI,UAAU,CAAC,CAAC,OACjD,WAAY,EAAK,QAAQ,EAAK,IAAQ,EAAM,EAAI,WAAY,CAAC,CAC/D,EACA,uDACF,CACF,CACF,EAGA,GAAM,CAAE,SAAU,GAAkB,MAAM,OAAO,qBAC3C,CAAE,kBAAiB,oBAAqB,MAAM,OAAO,iCAC3D,EAAY,EAAkB,KAAO,IAAQ,CAC3C,MAAM,EAAc,SAAY,CAC9B,IAAI,EAAgB,KACpB,GAAI,CACF,GAAM,CAAE,0BAA2B,MAAM,OAAO,qCAChD,EAAgB,EAAuB,CAAC,CAAC,aAC3C,MAAQ,CACN,EAAI,OAAO,KACT,CAAE,QAAS,mBAAoB,EAC/B,4DACF,CACF,CACA,MAAM,EAAgB,EAAI,QAAS,CAAE,MAAK,OAAQ,EAAI,OAAQ,eAAc,CAAC,CAC/E,CAAC,CACH,CAAC,CACH,CACF,EACA,YAAa,CAEX,gBAAiB,CAAC,GAAG,GAAkB,EAAc,EACrD,0BAA2B,CAAC,EAAe,EAI3C,sBAAuB,EACzB,EACA,OAAQ,CACN,SAAU,EAGV,cAAe,CAAC,GAAG,EAAc,EACjC,SAAU,CAAC,EAAa,EACxB,SAAU,CACZ,EACA,QAAS,CAEP,oBAAqB,EAAG,EAAK,MAAO,EAA4B,EAChE,MAAO,CACL,iBACA,kBACA,mBACA,sBACF,EACA,QAAS,CACP,CACE,GAAI,QACJ,MAAO,UACP,MAAO,QACP,KAAM,eACN,SAAU,iBAGV,SAAU,CACZ,CACF,EACA,cAAe,CACb,CAEE,KAAM,QACN,QAAS,gBACT,KAAM,CAIJ,SAAU,EACV,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,8CACb,eAAgB,yBAIhB,OAAQ,QACV,CACF,EACA,CAIE,KAAM,gBACN,QAAS,sBACT,KAAM,CAIJ,SAAU,EACV,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,2EACb,eAAgB,4BAKhB,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,EACA,CAGE,KAAM,yBACN,QAAS,kBACT,KAAM,CACJ,SAAU,EACV,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,qDACb,eAAgB,gCAGhB,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.102.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.",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"types": "./dist/index.d.mts",
|
|
10
10
|
"import": "./dist/index.mjs"
|
|
11
11
|
},
|
|
12
|
+
"./canvas-header-action": {
|
|
13
|
+
"types": "./dist/canvas-header-action.d.mts",
|
|
14
|
+
"import": "./dist/canvas-header-action.mjs"
|
|
15
|
+
},
|
|
12
16
|
"./pages": {
|
|
13
17
|
"types": "./dist/pages.d.mts",
|
|
14
18
|
"import": "./dist/pages.mjs"
|
|
@@ -22,13 +26,15 @@
|
|
|
22
26
|
"dist"
|
|
23
27
|
],
|
|
24
28
|
"dependencies": {
|
|
25
|
-
"@murumets-ee/admin-ui": "0.
|
|
26
|
-
"@murumets-ee/
|
|
27
|
-
"@murumets-ee/
|
|
28
|
-
"@murumets-ee/
|
|
29
|
-
"@murumets-ee/forms": "0.
|
|
30
|
-
"@murumets-ee/
|
|
31
|
-
"@murumets-ee/
|
|
29
|
+
"@murumets-ee/admin-ui": "0.102.0",
|
|
30
|
+
"@murumets-ee/blocks": "0.102.0",
|
|
31
|
+
"@murumets-ee/core": "0.102.0",
|
|
32
|
+
"@murumets-ee/content": "0.102.0",
|
|
33
|
+
"@murumets-ee/forms": "0.102.0",
|
|
34
|
+
"@murumets-ee/notifications": "0.102.0",
|
|
35
|
+
"@murumets-ee/settings": "0.102.0",
|
|
36
|
+
"@murumets-ee/queue": "0.102.0",
|
|
37
|
+
"@murumets-ee/ui": "0.102.0"
|
|
32
38
|
},
|
|
33
39
|
"peerDependencies": {
|
|
34
40
|
"next": ">=16.0.0 <17",
|
|
@@ -61,9 +67,10 @@
|
|
|
61
67
|
"atLeast": 100
|
|
62
68
|
},
|
|
63
69
|
"bundleBudget": {
|
|
64
|
-
"dist/index.mjs":
|
|
65
|
-
"dist/
|
|
66
|
-
"dist/
|
|
70
|
+
"dist/index.mjs": 20480,
|
|
71
|
+
"dist/canvas-header-action.mjs": 2048,
|
|
72
|
+
"dist/pages.mjs": 14336,
|
|
73
|
+
"dist/plugin.mjs": 20480
|
|
67
74
|
},
|
|
68
75
|
"scripts": {
|
|
69
76
|
"build": "tsdown",
|