@murumets-ee/forms-ui 0.101.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/index.d.mts +4 -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.map +1 -1
- package/dist/pages.mjs +1 -1
- package/dist/pages.mjs.map +1 -1
- package/dist/plugin.d.mts +2 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/package.json +12 -11
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { ReactElement } from "react";
|
|
3
|
-
import { SubmissionColumn, SubmissionRow } from "@murumets-ee/forms";
|
|
3
|
+
import { DeliveryRow, SubmissionColumn, SubmissionRow } from "@murumets-ee/forms";
|
|
4
4
|
|
|
5
5
|
//#region src/delete-form-dialog.d.ts
|
|
6
6
|
interface DeleteFormButtonProps {
|
|
@@ -64,6 +64,8 @@ interface SubmissionsViewProps {
|
|
|
64
64
|
readonly columns: readonly SubmissionColumn[];
|
|
65
65
|
/** May mark read / archive / delete (`form_submissions:manage`). */
|
|
66
66
|
readonly canManage: boolean;
|
|
67
|
+
/** Each listed submission's emails (notification / auto-reply), by submission id. */
|
|
68
|
+
readonly deliveries: Readonly<Record<string, readonly DeliveryRow[]>>;
|
|
67
69
|
readonly apiBasePath: string;
|
|
68
70
|
/** What to say when no submission is listed — the page knows whether a filter is why. */
|
|
69
71
|
readonly emptyText: string;
|
|
@@ -73,6 +75,7 @@ declare function SubmissionsView({
|
|
|
73
75
|
rows,
|
|
74
76
|
columns,
|
|
75
77
|
canManage,
|
|
78
|
+
deliveries,
|
|
76
79
|
apiBasePath,
|
|
77
80
|
emptyText
|
|
78
81
|
}: SubmissionsViewProps): ReactElement;
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;;;
|
|
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{Badge as e,Button as t,Checkbox as n,Dialog as r,DialogContent as i,DialogDescription as a,DialogHeader as o,DialogTitle as s,Input as c,Label as l,Table as u,TableBody as d,TableCell as f,TableHead as p,TableHeader as m,TableRow as h}from"@murumets-ee/ui";import{useRouter as g}from"next/navigation";import{useFormatter as _,useTranslations as v}from"next-intl";import{useEffect as y,useId as b,useRef as x,useState as S,useTransition as C}from"react";import{Fragment as w,jsx as T,jsxs as E}from"react/jsx-runtime";import{FORMS_SOURCE as D,FORM_ENVIRONMENT as O,FORM_NAME_MAX as k,PAGE_ENVIRONMENT as A,formEnvironmentBlocks as j,formEnvironmentTraits as M,formSiteBlocks as N}from"@murumets-ee/forms/schema";import{registerBlockEnvironment as ee,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:c,submissions:l,pages:u}){let d=v(`Forms.remove`),f=v(`Forms.list`),p=g(),[,m]=C(),[h,_]=S(!1),[y,b]=S(!1),[x,D]=S(null),[O,k]=S(!1),A=u!==null&&u>0,j=()=>{_(!1),O&&m(()=>p.refresh())},M=async()=>{b(!0),D(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){D(d(`awaitingApproval`));return}if(t.status===409){D(d(`stillInUse`));return}if(t.status===403){D(d(`forbidden`));return}let r=await t.json().catch(()=>null),i=typeof r?.submissions==`number`?r.submissions:0;if(t.status===500&&r?.deleted===!0){k(!0),D(d(`lateFailed`));return}if(m(()=>p.refresh()),t.status===500&&r?.more===!0){D(d(`partial`,{count:i}));return}if(!t.ok){D(d(`failed`,{status:t.status}));return}if(r?.deleted!==!0){D(d(`more`,{count:i}));return}_(!1)}catch{D(d(`failed`,{status:0}))}finally{b(!1)}};return E(w,{children:[T(t,{type:`button`,variant:`ghost`,size:`sm`,className:`text-destructive hover:text-destructive`,"aria-label":f(`deleteForm`,{form:c}),onClick:()=>{D(null),_(!0)},children:f(`delete`)}),T(r,{open:h,onOpenChange:e=>e?_(!0):j(),children:E(i,{className:`max-w-md`,children:[T(o,{children:T(s,{children:d(`title`,{form:c})})}),T(a,{asChild:!0,children:T(`div`,{className:`space-y-2 text-sm text-muted-foreground`,children:A?T(`p`,{"data-forms-remove-in-use":``,children:d(`inUse`,{count:u})}):E(w,{children:[T(`p`,{"data-forms-remove-count":l??``,children:l===null?d(`submissionsUnknown`):d(`submissions`,{count:l})}),T(`p`,{children:d(`trash`)})]})})}),x!==null&&T(`p`,{role:`alert`,className:`text-sm text-destructive`,children:x}),E(`div`,{className:`flex justify-end gap-2 pt-2`,children:[T(t,{type:`button`,variant:`outline`,onClick:j,children:d(`cancel`)}),!A&&!O&&T(t,{variant:`destructive`,disabled:y,onClick:()=>void M(),children:d(l===0?`confirmEmpty`:`confirm`)})]})]})})]})}function W({apiBasePath:e,formsBasePath:n}){let a=v(`Forms.newDialog`),u=g(),[d,f]=S(!1),[p,m]=S(``),[h,_]=S(!1),[y,b]=S(null),x=p.trim(),C=x!==``&&x.length<=k&&!h;return E(w,{children:[T(t,{type:`button`,size:`sm`,onClick:()=>f(!0),children:a(`title`)}),T(r,{open:d,onOpenChange:f,children:E(i,{className:`max-w-md`,children:[T(o,{children:T(s,{children:a(`title`)})}),E(`form`,{onSubmit:async t=>{if(t.preventDefault(),C){_(!0),b(null);try{let t=await fetch(`${e}/form`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:x})});if(!t.ok)throw Error(a(`failed`,{status:t.status}));let r=await t.json();if(typeof r.id!=`string`)throw Error(a(`noId`));f(!1),_(!1),u.push(`${n}/${r.id}`)}catch(e){b(e instanceof Error?e.message:a(`failed`,{status:0})),_(!1)}}},className:`flex flex-col gap-3`,children:[E(`div`,{className:`flex flex-col gap-1.5`,children:[T(l,{htmlFor:`new-form-name`,children:a(`name`)}),T(c,{id:`new-form-name`,value:p,maxLength:k,onChange:e=>m(e.target.value),placeholder:a(`namePlaceholder`),"aria-describedby":`new-form-name-help`}),T(`p`,{id:`new-form-name-help`,className:`text-xs text-muted-foreground`,children:a(`help`)})]}),y!==null&&T(`p`,{role:`alert`,className:`text-sm text-destructive`,children:y}),E(`div`,{className:`flex justify-end gap-2 pt-1`,children:[T(t,{type:`button`,variant:`outline`,onClick:()=>f(!1),children:a(`cancel`)}),T(t,{type:`submit`,disabled:!C,loading:h,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 q(e,t){return/filename="([^"]+)"/.exec(e.headers.get(`Content-Disposition`)??``)?.[1]??t}function J({apiBasePath:e,formsBasePath:n,locale:g,canErase:y}){let x=v(`Forms.privacy`),C=_(),D=b(),[O,k]=S(``),[A,j]=S(null),[M,N]=S(!1),[ee,P]=S(!1),[F,I]=S(null),[L,R]=S(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(x(`invalid`)):e.status===403?I(x(`forbidden`)):e.status===503?I(x(`busy`)):I(x(`failed`,{status:e.status})),!1),H=async e=>{N(!0),I(null);try{let t=await B(`/search`,`POST`,{email:e});if(!V(t)){j(null);return}let n=await t.json().catch(()=>null);if(!G(n)||typeof n.total!=`number`||!Array.isArray(n.items)){I(x(`failed`,{status:t.status})),j(null);return}let r=n.items;j({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(x(`failed`,{status:0})),j(null)}finally{N(!1)}},U=e=>{e.preventDefault();let t=O.trim();if(R(null),t===``){I(x(`invalid`)),j(null);return}H(t)},W=async e=>{if(A!==null){N(!0),I(null),R(null);try{let t=await B(`/export`,`POST`,{email:A.email,format:e,uiLocale:g});if(!V(t))return;let n=URL.createObjectURL(await t.blob()),r=document.createElement(`a`);r.href=n,r.download=q(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(x(`exportCapped`,{max:A.exportMax}))}catch{I(x(`failed`,{status:0}))}finally{N(!1)}}},J=async()=>{if(A!==null){P(!1),N(!0),I(null),R(null);try{let e=await B(``,`DELETE`,{email:A.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(A.email),I(x(`eraseFailed`,{count:n}));return}if(!V(e))return;if(n===null){I(x(`failed`,{status:e.status}));return}let r=G(t)&&t.more===!0;await H(A.email),R(x(r?`erasedMore`:`erased`,{count:n}))}catch{I(x(`failed`,{status:0}))}finally{N(!1)}}},Y=e=>C.dateTime(new Date(e),{dateStyle:`short`,timeStyle:`short`});return E(`div`,{className:`space-y-4`,children:[E(`form`,{onSubmit:U,className:`flex flex-wrap items-end gap-3 rounded-md border p-3`,autoComplete:`off`,children:[E(`div`,{className:`flex min-w-[16rem] flex-1 flex-col gap-1.5`,children:[T(l,{htmlFor:D,children:x(`email`)}),T(c,{id:D,type:`email`,inputMode:`email`,autoComplete:`off`,spellCheck:!1,maxLength:320,value:O,onChange:e=>k(e.target.value),placeholder:x(`placeholder`)})]}),T(t,{type:`submit`,disabled:M,children:x(M&&A===null?`finding`:`find`)})]}),F!==null&&T(`p`,{role:`alert`,className:`rounded-md border border-destructive/40 p-3 text-sm`,children:F}),L!==null&&T(`p`,{role:`status`,className:`rounded-md border p-3 text-sm`,"data-forms-privacy-notice":``,children:L}),A!==null&&A.unindexed>0&&T(`p`,{role:`status`,className:`rounded-md border border-amber-500/50 bg-amber-500/10 p-3 text-sm`,"data-forms-privacy-unindexed":A.unindexed,children:x(`unindexed`,{count:A.unindexed})}),A!==null&&A.total===0&&E(`div`,{className:`rounded-md border border-dashed p-6 text-sm text-muted-foreground`,children:[T(`p`,{children:x(`none`)}),T(`p`,{className:`mt-1`,children:x(`noneHint`)})]}),A!==null&&A.total>0&&E(w,{children:[T(`div`,{className:`overflow-x-auto rounded-md border`,children:E(u,{children:[T(m,{children:E(h,{children:[T(p,{children:x(`form`)}),T(p,{children:x(`received`)}),T(p,{children:x(`page`)})]})}),T(d,{children:A.items.map(e=>E(h,{"data-forms-privacy-row":e.id,children:[T(f,{className:`font-medium`,children:e.formName===null?T(`span`,{className:`text-muted-foreground`,children:x(`deletedForm`)}):T(`a`,{href:`${n}/${e.formId}/submissions`,className:`text-primary hover:underline`,children:e.formName})}),T(f,{className:`whitespace-nowrap tabular-nums`,children:Y(e.createdAt)}),T(f,{className:`text-muted-foreground`,children:e.pagePath??`—`})]},e.id))})]})}),E(`div`,{className:`flex flex-wrap items-center gap-2 text-sm`,children:[T(`span`,{"data-forms-privacy-total":A.total,children:x(`summary`,{count:A.total})}),T(t,{variant:`outline`,size:`sm`,disabled:M,onClick:()=>void W(`csv`),children:x(`exportCsv`)}),T(t,{variant:`outline`,size:`sm`,disabled:M,onClick:()=>void W(`json`),children:x(`exportJson`)}),y&&T(t,{variant:`outline`,size:`sm`,className:`border-destructive/50 text-destructive hover:bg-destructive/10`,disabled:M,onClick:()=>P(!0),children:x(`eraseAll`,{count:A.total})})]}),A.total>A.items.length&&T(`p`,{className:`text-xs text-muted-foreground`,children:x(`listCapped`,{shown:A.items.length,max:A.exportMax})}),T(`p`,{className:`text-xs text-muted-foreground`,children:x(`eraseNote`)})]}),T(r,{open:ee,onOpenChange:P,children:E(i,{className:`max-w-md`,children:[T(o,{children:T(s,{children:x(`confirmTitle`,{count:Math.max(A?.total??1,1)})})}),T(a,{children:x(`confirmBody`)}),E(`div`,{className:`flex justify-end gap-2 pt-2`,children:[T(t,{variant:`outline`,onClick:()=>P(!1),children:x(`cancel`)}),T(t,{variant:`destructive`,disabled:M,onClick:()=>void J(),children:x(`confirmErase`)})]})]})})]})}function Y(){F(V),P({name:O,blocks:j,traits:M,fieldTypes:[I(L),I(z)],defaultTheme:B}),ee({source:D,name:A,blocks:N,fieldTypes:[I(R),I(L),I(z)]})}function X(){return Y(),null}const te={new:`default`,read:`secondary`,archived:`outline`};function Z(e,t){return Object.hasOwn(e,t)?e[t]:void 0}function ne(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 re({formId:c,rows:l,columns:b,canManage:w,apiBasePath:D,emptyText:O}){let k=v(`Forms.submissions`),A=v(`Forms.submissionStatus`),j=_(),M=g(),[N,ee]=C(),[P,F]=S(null),I=P?.id??null,L=x(null),R=x(new Set),[z,B]=S(new Set),[V,H]=S(null),[U,W]=S(!1),[G,K]=S(null),q=l.find(e=>e.id===I)??P,J=l.filter(e=>z.has(e.id)).map(e=>e.id),Y=l.length>0&&J.length===l.length,X=e=>j.dateTime(new Date(e),{dateStyle:`short`,timeStyle:`short`}),re=e=>e===void 0||e===``?`—`:typeof e==`boolean`?k(e?`yes`:`no`):String(e),ie=async(e,t)=>{W(!0),K(null);try{let n=await fetch(`${D}/form-submissions`,{method:e,headers:{"Content-Type":`application/json`},body:JSON.stringify({form:c,...t})});if(n.status===202)return K(k(`awaitingApproval`)),!1;if(!n.ok)return K(k(`failed`,{status:n.status})),!1;let r=await n.json().catch(()=>null),i=e===`DELETE`?r?.deleted:r?.changed;if(ee(()=>M.refresh()),typeof i!=`number`||i<=0){K(k(`nothingChanged`));let e=t.ids;return Array.isArray(e)&&F(t=>t&&e.includes(t.id)?null:t),!1}return!0}catch{return K(k(`failed`,{status:0})),!1}finally{W(!1)}},Q=async(e,t)=>{let n=await ie(`PATCH`,{ids:e,status:t});if(!n||t===`new`)for(let t of e)R.current.delete(t);return n&&F(n=>n&&e.includes(n.id)?{...n,status:t}:n),n},ae=e=>{F(e),w&&e.status===`new`&&!R.current.has(e.id)&&(R.current.add(e.id),Q([e.id],`read`))};y(()=>{I!==null&&L.current?.focus()},[I]);let oe=async()=>{if(V===null)return;let e=V;H(null),await ie(`DELETE`,{ids:e})&&(B(t=>new Set([...t].filter(t=>!e.includes(t)))),F(t=>t&&e.includes(t.id)?null:t))},se=(e,t)=>B(n=>{let r=new Set(n);return t?r.add(e):r.delete(e),r}),$=U||N;return E(`div`,{className:`space-y-3`,children:[G!==null&&T(`p`,{role:`alert`,className:`rounded-md border border-destructive/40 p-3 text-sm`,children:G}),E(`div`,{className:`flex flex-col gap-4 lg:flex-row lg:items-start`,children:[E(`div`,{className:`min-w-0 flex-1 space-y-3`,children:[l.length===0&&T(`p`,{className:`rounded-md border border-dashed p-6 text-sm text-muted-foreground`,children:O}),T(`div`,{className:`overflow-x-auto rounded-md border`,hidden:l.length===0,children:E(u,{children:[T(m,{children:E(h,{children:[w&&T(p,{className:`w-10`,children:T(n,{"aria-label":k(`selectAll`),checked:Y,onCheckedChange:e=>B(e===!0?new Set(l.map(e=>e.id)):new Set)})}),T(p,{className:`whitespace-nowrap`,children:k(`received`)}),T(p,{className:`whitespace-nowrap`,children:k(`status`)}),T(p,{className:`whitespace-nowrap`,children:k(`page`)}),b.map(e=>E(p,{"data-forms-column":e.key,title:e.label,className:`max-w-[14rem] truncate whitespace-nowrap`,children:[e.label,e.retired&&E(`span`,{className:`ml-1 text-xs font-normal text-muted-foreground`,children:[`(`,k(`removedField`),`)`]})]},e.key))]})}),T(d,{children:l.map(t=>E(h,{"data-state":t.id===I?`selected`:void 0,className:t.status===`new`?`font-medium`:void 0,children:[w&&T(f,{children:T(n,{"aria-label":k(`selectRow`,{when:X(t.createdAt)}),checked:z.has(t.id),onCheckedChange:e=>se(t.id,e===!0)})}),T(f,{className:`whitespace-nowrap`,children:T(`button`,{type:`button`,className:`text-primary hover:underline`,"aria-expanded":t.id===I,onClick:()=>ae(t),children:X(t.createdAt)})}),T(f,{children:T(e,{variant:te[t.status],children:A(t.status)})}),T(f,{className:`text-muted-foreground`,children:t.pagePath??`—`}),b.map(e=>{let n=re(Z(t.data,e.key));return T(f,{className:`max-w-[16rem] truncate`,children:n.length>60?`${n.slice(0,60)}…`:n},e.key)})]},t.id))})]})}),w&&l.length>0&&T(t,{variant:`outline`,size:`sm`,disabled:$||J.length===0,onClick:()=>H(J),children:k(`deleteSelected`,{count:J.length})})]}),q!==null&&E(`aside`,{ref:L,tabIndex:-1,"aria-label":k(`detail`),className:`w-full shrink-0 space-y-4 rounded-md border p-4 lg:w-96`,children:[E(`div`,{className:`flex items-start justify-between gap-2`,children:[E(`div`,{children:[T(`p`,{className:`font-medium`,children:X(q.createdAt)}),T(`p`,{className:`text-sm text-muted-foreground`,children:q.pagePath===null?k(`noPage`):k(`fromPage`,{page:q.pagePath})})]}),T(t,{variant:`ghost`,size:`sm`,onClick:()=>F(null),children:k(`close`)})]}),T(`dl`,{className:`space-y-3 text-sm`,children:ne(q,b).map(e=>{let t=Z(q.data,e),n=Z(q.consent,e);return E(`div`,{children:[T(`dt`,{className:`text-xs text-muted-foreground`,children:Z(q.fields,e)?.label??e}),T(`dd`,{className:`whitespace-pre-wrap break-words`,children:re(t)}),n!==void 0&&T(`dd`,{className:`mt-1 text-xs text-muted-foreground`,children:k(`consentWording`,{wording:n})})]},e)})}),w&&E(`div`,{className:`flex flex-wrap gap-2 border-t pt-3`,children:[q.status===`new`?T(t,{variant:`outline`,size:`sm`,disabled:$,onClick:()=>void Q([q.id],`read`),children:k(`markRead`)}):T(t,{variant:`outline`,size:`sm`,disabled:$,onClick:()=>void Q([q.id],`new`),children:k(`markNew`)}),q.status===`archived`?T(t,{variant:`outline`,size:`sm`,disabled:$,onClick:()=>void Q([q.id],`read`),children:k(`unarchive`)}):T(t,{variant:`outline`,size:`sm`,disabled:$,onClick:()=>void Q([q.id],`archived`),children:k(`archive`)}),T(t,{variant:`destructive`,size:`sm`,disabled:$,onClick:()=>H([q.id]),children:k(`delete`)})]})]})]}),T(r,{open:V!==null,onOpenChange:e=>!e&&H(null),children:E(i,{children:[T(o,{children:T(s,{children:k(`confirmDeleteTitle`,{count:V?.length??0})})}),T(a,{children:k(`confirmDeleteBody`)}),E(`div`,{className:`flex justify-end gap-2`,children:[T(t,{variant:`outline`,onClick:()=>H(null),children:k(`cancel`)}),T(t,{variant:`destructive`,disabled:U,onClick:()=>void oe(),children:k(`confirmDelete`)})]})]})})]})}function ie({href:e}){return T(t,{variant:`outline`,size:`sm`,asChild:!0,children:T(`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:v(`Forms.submissions`)(`exportCsv`)})})}export{U as DeleteFormButton,X as FormBlocksRegistration,W as NewFormDialog,J as PrivacyRequestsView,ie as SubmissionsExportLink,re as SubmissionsView,Y 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/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 { SubmissionColumn, SubmissionRow, SubmissionStatus } 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 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/** `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 apiBasePath,\n emptyText,\n}: SubmissionsViewProps): ReactElement {\n const t = useTranslations('Forms.submissions')\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\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 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\n return (\n <div className=\"space-y-3\">\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\">{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 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 {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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"}
|
|
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.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","../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;;;
|
|
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{getApp as i,getContext as a,isTokenDerivedDoor as o,requireRequestSite as s}from"@murumets-ee/core";import{FORMS_RESOURCE as c,FORMS_SETTINGS_NAMESPACE as l,FORM_ENTITY as u,FORM_SUBMISSIONS_RESOURCE as d,Form as f,STATUS_FILTERS as p,SUBMISSIONS_PAGE_SIZE as m,collectRetiredColumns as h,currentFormFields as g,formSpamCounts as _,formsSettings as v,lastRetentionRun as y,listSubmissions as b,parseSubmissionFilters as x,submissionColumns as S,submissionCounts as C,submissionFilterParams as w,submissionPages as ee}from"@murumets-ee/forms";import{FormCanvasFactsProvider as T}from"@murumets-ee/forms/editor";import{notFound as E,redirect as D}from"next/navigation";import{challengeSecretFor as O}from"@murumets-ee/forms/server";import{SettingsClient as k}from"@murumets-ee/settings";import{jsx as A,jsxs as j}from"react/jsx-runtime";import{fetchEntityList as M}from"@murumets-ee/admin-ui/server";import{WorkflowStatusBadge as N}from"@murumets-ee/admin-ui/workflow";import{countEmbedUsages as P}from"@murumets-ee/blocks/server";import{DeleteFormButton as F,NewFormDialog as I,PrivacyRequestsView as L,SubmissionsExportLink as R,SubmissionsView as z}from"@murumets-ee/forms-ui";import{Button as B,Table as V,TableBody as H,TableCell as U,TableHead as W,TableHeader as G,TableRow as K}from"@murumets-ee/ui";import q from"next/link";import{getFormatter as J,getTimeZone as Y,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 _.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 te(){let e=i(),t=await s(l,e),n=new k(v,{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)&&O(`turnstile`,t,process.env)!==void 0,recaptcha:o(a)&&O(`recaptcha`,t,process.env)!==void 0}}const ne=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function re({params:t,searchParams:a}){let{id:o,locale:s}=await t;ne.test(o)||E();let c=e(),u=await r(f,c,{params:t,searchParams:a}),p=await c.withAdminContext(s,async()=>{let e=c.adminBasePath.replace(`:locale`,s),[t,r,a]=await Promise.all([Q([o]).catch(()=>(console.warn(`forms: the blocked count for form ${o} could not be read`),null)),te(),n(d,`view`,c).then(async e=>e?(await C(i(),[o])).get(o)?.fresh??0:null).catch(()=>(console.warn(`forms: the new-submissions count for form ${o} could not be read`),null))]);return{discarded:t===null?null:t.get(o)??0,ready:r,settingsHref:`${e}/settings/${l}`,...a!==null&&{submissions:{href:`${e}/forms/${o}/submissions`,fresh:a}}}}).catch(()=>(console.warn(`forms: the spam facts for form ${o} could not be read`),null));return p===null?u:A(T,{facts:p,children:u})}async function ie({params:r}){let a=e(),{locale:o}=await r;Z(o);let s=await X(`Forms.list`),l=`${a.adminBasePath.replace(`:locale`,o)}/forms`;return a.withAdminContext(o,async()=>{await t(f,`view`,a)||E();let[{items:e,total:r},o,p,m,h]=await Promise.all([M(f,{sortField:`name`,sortDirection:`asc`,limit:200}),n(`form`,`create`,a),n(`form`,`delete`,a),n(c,`manage`,a),n(d,`view`,a)]),g=a.apiBasePath??`/api/admin`,_=await P(u,e.map(e=>e.id)).catch(()=>null),v=m||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,b=h?await C(i(),e.map(e=>String(e.id))).catch(()=>(console.warn(`forms: the submission counts for the Forms list could not be read`),null)):null,x=e=>`${l}/${String(e)}/submissions`,S=h?await y(i()).catch(()=>{console.warn(`forms: the last clean-up run for the Forms list could not be read`)}):void 0,w=await J();return j(`div`,{className:`space-y-4 p-6`,children:[j(`div`,{className:`flex items-start justify-between gap-4`,children:[j(`div`,{children:[A(`h1`,{className:`text-2xl font-bold`,children:s(`title`)}),A(`p`,{className:`text-sm text-muted-foreground`,children:s(`description`)})]}),j(`div`,{className:`flex items-center gap-2`,children:[h&&A(B,{variant:`outline`,size:`sm`,asChild:!0,children:A(q,{href:`${l}/privacy`,"data-forms-privacy-link":``,children:s(`privacy`)})}),o&&m&&A(I,{apiBasePath:g,formsBasePath:l})]})]}),e.length===0?A(`p`,{className:`rounded-md border border-dashed p-6 text-sm text-muted-foreground`,children:s(`empty`)}):j(V,{children:[A(G,{children:j(K,{children:[A(W,{children:s(`name`)}),A(W,{children:s(`usedOn`)}),A(W,{className:`text-right`,children:s(`submissions`)}),A(W,{className:`text-right`,children:s(`new`)}),A(W,{className:`text-right`,children:s(`blocked`)}),A(W,{children:s(`status`)}),p&&m&&A(W,{className:`w-0`})]})}),A(H,{children:e.map(e=>j(K,{children:[A(U,{className:`font-medium`,children:m||h?A(q,{href:m?`${l}/${e.id}`:x(e.id),className:`text-primary hover:underline`,children:e.name}):e.name}),A(U,{className:`text-muted-foreground`,"data-forms-used-on":e.id,children:_===null?s(`notYet`):s(`usedOnPages`,{count:_.get(e.id)??0})}),A(U,{className:`text-right tabular-nums`,"data-forms-submissions":e.id,children:b===null?A(`span`,{className:`text-muted-foreground`,children:s(`notYet`)}):A(q,{href:x(e.id),className:`text-primary hover:underline`,"aria-label":s(`submissionsOf`,{form:String(e.name),count:b.get(String(e.id))?.total??0}),children:b.get(String(e.id))?.total??0})}),A(U,{className:`text-right tabular-nums`,"data-forms-new":e.id,children:b===null?A(`span`,{className:`text-muted-foreground`,children:s(`notYet`)}):b.get(String(e.id))?.fresh??0}),A(U,{className:`text-right tabular-nums`,children:v===null?s(`notYet`):v.get(String(e.id))??0}),A(U,{children:A(N,{status:e.status===`published`?`published`:`draft`})}),p&&m&&A(U,{className:`text-right`,children:A(F,{apiBasePath:g,formId:String(e.id),formName:String(e.name),submissions:b===null?null:b.get(String(e.id))?.total??0,pages:_===null?null:_.get(e.id)??0})})]},e.id))})]}),r>e.length&&A(`p`,{className:`text-xs text-muted-foreground`,children:s(`truncated`,{count:e.length})}),S!==void 0&&A(`p`,{className:`text-xs text-muted-foreground`,"data-forms-retention-run":``,children:S===null?s(`retentionNever`):`${s(`retentionRun`,{when:w.dateTime(new Date(S.lastRunAt),{dateStyle:`medium`,timeStyle:`short`}),count:S.removed})}${S.unfinished?` ${s(`retentionUnfinished`)}`:``}`})]})})}async function ae({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(d,`view`,r)||E();let e=!o(a()?.door)&&await n(d,`manage`,r);return j(`div`,{className:`space-y-4 p-6`,children:[j(`div`,{children:[A(`p`,{className:`text-sm text-muted-foreground`,children:j(q,{href:c,className:`hover:underline`,children:[`← `,s(`backToForms`)]})}),A(`h1`,{className:`text-2xl font-bold`,children:s(`title`)}),A(`p`,{className:`max-w-prose text-sm text-muted-foreground`,children:s(`intro`)})]}),A(L,{apiBasePath:r.apiBasePath??`/api/admin`,formsBasePath:c,locale:i,canErase:e})]})})}const oe=/^[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 se({params:t,searchParams:r}){let{id:a,locale:o}=await t;oe.test(a)||E();let s=await r,l=e();Z(o);let u=await X(`Forms.submissions`),_=`${l.adminBasePath.replace(`:locale`,o)}/forms`,v=l.apiBasePath??`/api/admin`;return l.withAdminContext(o,async()=>{await n(d,`view`,l)||E();let e=i(),t=await e.getSystemClient(f).findById(a);t??E();let r=x(e=>$(s[e]),{timeZone:await Y()}),y=Number($(s.p)),C=Number.isFinite(y)?Math.min(Math.max(Math.trunc(y),1),1e5):1,[{items:T,total:O},k,M,N,P]=await Promise.all([b(e,a,r,{limit:m,offset:(C-1)*m}),ee(e,a),n(d,`manage`,l),n(c,`manage`,l),Q([a]).catch(()=>(console.warn(`forms: the blocked count for form ${a} could not be read`),null))]),F=g(t.body),I=S(F,h(F,T)),L=w(r),V=new URLSearchParams(L);V.set(`form`,a),V.set(`uiLocale`,o),V.set(`tz`,r.timeZone);let H=e=>{let t=new URLSearchParams(L);e>1&&t.set(`p`,String(e));let n=t.toString();return`${_}/${a}/submissions${n===``?``:`?${n}`}`},U=Math.max(Math.ceil(O/m),1);T.length===0&&O>0&&C>U&&D(H(U));let W=L.toString()!==``,G=typeof t.name==`string`?t.name:``,K=P===null?null:P.get(a)??0,J=r.page!==void 0&&!k.includes(r.page)?[r.page,...k]:k;return j(`div`,{className:`space-y-4 p-6`,children:[j(`div`,{className:`flex flex-wrap items-start justify-between gap-4`,children:[j(`div`,{children:[A(`p`,{className:`text-sm text-muted-foreground`,children:j(q,{href:_,className:`hover:underline`,children:[`← `,u(`backToForms`)]})}),A(`h1`,{className:`text-2xl font-bold`,children:u(`title`,{form:G})})]}),j(`div`,{className:`flex items-center gap-2`,children:[N&&A(B,{variant:`outline`,size:`sm`,asChild:!0,children:A(q,{href:`${_}/${a}`,children:u(`openForm`)})}),A(R,{href:`${v}/form-submissions/export?${V.toString()}`})]})]}),j(`form`,{method:`get`,action:`${_}/${a}/submissions`,className:`flex flex-wrap items-end gap-3 rounded-md border p-3 text-sm`,children:[j(`label`,{className:`flex flex-col gap-1`,children:[A(`span`,{className:`text-xs text-muted-foreground`,children:u(`filters.from`)}),A(`input`,{type:`date`,name:`from`,defaultValue:r.from??``,className:`h-8 rounded-md border bg-background px-2`})]}),j(`label`,{className:`flex flex-col gap-1`,children:[A(`span`,{className:`text-xs text-muted-foreground`,children:u(`filters.to`)}),A(`input`,{type:`date`,name:`to`,defaultValue:r.to??``,className:`h-8 rounded-md border bg-background px-2`})]}),j(`label`,{className:`flex flex-col gap-1`,children:[A(`span`,{className:`text-xs text-muted-foreground`,children:u(`filters.page`)}),j(`select`,{name:`page`,defaultValue:r.page??``,className:`h-8 rounded-md border bg-background px-2`,children:[A(`option`,{value:``,children:u(`filters.allPages`)}),J.map(e=>A(`option`,{value:e,children:e},e))]})]}),j(`label`,{className:`flex flex-col gap-1`,children:[A(`span`,{className:`text-xs text-muted-foreground`,children:u(`filters.status`)}),A(`select`,{name:`status`,defaultValue:r.status,className:`h-8 rounded-md border bg-background px-2`,children:p.map(e=>A(`option`,{value:e,children:u(`filters.statuses.${e}`)},e))})]}),A(B,{type:`submit`,size:`sm`,variant:`secondary`,children:u(`filters.apply`)}),W&&A(B,{size:`sm`,variant:`ghost`,asChild:!0,children:A(q,{href:`${_}/${a}/submissions`,children:u(`filters.clear`)})}),K!==null&&K>0&&A(`span`,{className:`ml-auto text-xs text-muted-foreground`,"data-forms-blocked":!0,children:u(`blocked`,{count:K})})]}),A(z,{formId:a,rows:T,columns:I,canManage:M,apiBasePath:v,emptyText:u(W?`emptyFiltered`:`empty`)},`${L.toString()}#${C}`),j(`div`,{className:`flex items-center justify-between text-sm text-muted-foreground`,children:[A(`span`,{children:u(`showing`,{shown:T.length,total:O})}),U>1&&j(`span`,{className:`flex items-center gap-2`,children:[C>1&&j(q,{href:H(C-1),className:`hover:underline`,children:[`← `,u(`previous`)]}),A(`span`,{children:u(`pageOf`,{page:Math.min(C,U),pages:U})}),C<U&&j(q,{href:H(C+1),className:`hover:underline`,children:[u(`next`),` →`]})]})]})]})})}export{re as FormCanvasPage,ie as FormsListPage,ae as PrivacyRequestsPage,se as SubmissionsPage};
|
|
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
|