domma-cms 1.10.16 → 1.10.18
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/CHANGELOG.md +9 -0
- package/admin/js/lib/collection-editor-arrange.js +1 -1
- package/admin/js/lib/collection-entries-arrange.js +1 -1
- package/admin/js/lib/form-submissions-arrange.js +1 -1
- package/admin/js/lib/media-arrange.js +1 -1
- package/admin/js/lib/media-picker.js +17 -5
- package/admin/js/lib/media-upload.js +1 -0
- package/admin/js/templates/collection-entries.html +4 -0
- package/admin/js/templates/form-submissions.html +3 -0
- package/admin/js/views/collection-entries.js +7 -7
- package/admin/js/views/form-editor.js +6 -6
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/media.js +3 -3
- package/docs/DommaFeatures.md +5 -4
- package/docs/api-reference.md +6 -1
- package/docs/configuration.md +55 -12
- package/docs/guides/multi-page-forms.md +1 -1
- package/docs/plugin-development.md +3 -0
- package/package.json +1 -1
- package/public/js/form-files.mjs +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/site.js +1 -1
- package/server/routes/api/forms.js +144 -40
- package/server/services/collections.js +3 -0
- package/server/services/email.js +15 -4
- package/server/services/forms.js +48 -3
- package/server/services/markdown.js +22 -10
- package/server/templates/page.html +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.10.18] - 2026-10-25
|
|
4
|
+
|
|
5
|
+
- **A form's files are checked against the per-submission size cap in the browser.** A form with a file field now tells the page `uploads.maxSubmissionBytes`, so a file that would take the form's files past it is refused as it is dropped or picked ("… the files together would be more than 50 MB"), counting files kept on earlier wizard steps, and the total is checked again before posting - in the `[form]` shortcode and embedded forms alike. The server still enforces it.
|
|
6
|
+
|
|
7
|
+
## [1.10.17] - 2026-10-25
|
|
8
|
+
|
|
9
|
+
- **Form file fields can take several files.** "Several files" and "Most files" in the form editor (`multiple`, `file.maxFiles`, default 5, at most 20). Every file is kept - stored as a list of `{url, name, size, mime}`; a single-file field is unchanged - and every file passes the type and size checks. Submissions, collection entries, `[collection]` displays, the email notification and the CSV export show each file; email links are full addresses. One submission's files are capped at `uploads.maxSubmissionBytes` (default the larger of 50 MB and `uploads.maxFileSize`). Files already written are removed when a submission fails after all, and the submissions CSV guards every cell against spreadsheet formulas. A file field's value now only ever comes from an uploaded file.
|
|
10
|
+
- **The media picker uploads like the Media screen** - drop, paste, per-file progress, cancel and Retry, and the same limits and "Replace?" question. One file uploaded becomes the choice; several stay listed to pick from. Closing the picker mid-upload lets the files finish into Media.
|
|
11
|
+
|
|
3
12
|
## [1.10.16] - 2026-10-25
|
|
4
13
|
|
|
5
14
|
- **Built against domma-js 0.58.1** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.58.1).
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export const FIELD_TYPES=[{value:"string",label:"Text (single line)"},{value:"email",label:"Email"},{value:"tel",label:"Phone"},{value:"number",label:"Number"},{value:"textarea",label:"Textarea (multi-line)"},{value:"select",label:"Dropdown (select)"},{value:"radio",label:"Radio buttons"},{value:"checkbox",label:"Single checkbox"},{value:"checkbox-group",label:"Checkbox group"},{value:"date",label:"Date"},{value:"time",label:"Time"},{value:"url",label:"URL"},{value:"hidden",label:"Hidden field"}],OPTION_TYPES=new Set(["select","radio","checkbox-group"]),ADDON_TYPES=new Set(["string","email","tel","number","textarea","select","date","time","url"]);const f=/^[a-z0-9-]+$/i;export const EXTRA_TYPES={clear:new Set(["string","email","tel","url","textarea"]),stepper:new Set(["number"]),counter:new Set(["string","textarea"])};const m=24;function p(e){return e?typeof e=="string"?{icon:"",text:e}:{icon:e.icon||"",text:e.icon?"":String(e.text??"")}:{icon:"",text:""}}export function addonFromBoxes(e,r){const n=String(e??"").trim(),t=String(r??"").trim();return n&&f.test(n)?{icon:n}:t?{text:t.slice(0,m)}:null}export const OPERATIONS=["create","read","update","delete"],EXPORT_FORMATS=["csv","json"],FIELD_NAME_RE=/^[A-Za-z_][A-Za-z0-9_-]*$/;export function slugify(e){return String(e??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}export const nameFromLabel=e=>slugify(e).replace(/-/g,"_"),typeLabel=e=>FIELD_TYPES.find(r=>r.value===e)?.label||e||"";export function optionsText(e){return(Array.isArray(e)?e:[]).map(r=>typeof r=="string"?`${r}: ${r}`:`${r?.value??""}: ${r?.label??r?.value??""}`).join(`
|
|
1
|
+
export const FIELD_TYPES=[{value:"string",label:"Text (single line)"},{value:"email",label:"Email"},{value:"tel",label:"Phone"},{value:"number",label:"Number"},{value:"textarea",label:"Textarea (multi-line)"},{value:"select",label:"Dropdown (select)"},{value:"radio",label:"Radio buttons"},{value:"checkbox",label:"Single checkbox"},{value:"checkbox-group",label:"Checkbox group"},{value:"date",label:"Date"},{value:"time",label:"Time"},{value:"url",label:"URL"},{value:"hidden",label:"Hidden field"},{value:"file",label:"Files (uploaded with a form)"}],OPTION_TYPES=new Set(["select","radio","checkbox-group"]),ADDON_TYPES=new Set(["string","email","tel","number","textarea","select","date","time","url"]);const f=/^[a-z0-9-]+$/i;export const EXTRA_TYPES={clear:new Set(["string","email","tel","url","textarea"]),stepper:new Set(["number"]),counter:new Set(["string","textarea"])};const m=24;function p(e){return e?typeof e=="string"?{icon:"",text:e}:{icon:e.icon||"",text:e.icon?"":String(e.text??"")}:{icon:"",text:""}}export function addonFromBoxes(e,r){const n=String(e??"").trim(),t=String(r??"").trim();return n&&f.test(n)?{icon:n}:t?{text:t.slice(0,m)}:null}export const OPERATIONS=["create","read","update","delete"],EXPORT_FORMATS=["csv","json"],FIELD_NAME_RE=/^[A-Za-z_][A-Za-z0-9_-]*$/;export function slugify(e){return String(e??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}export const nameFromLabel=e=>slugify(e).replace(/-/g,"_"),typeLabel=e=>FIELD_TYPES.find(r=>r.value===e)?.label||e||"";export function optionsText(e){return(Array.isArray(e)?e:[]).map(r=>typeof r=="string"?`${r}: ${r}`:`${r?.value??""}: ${r?.label??r?.value??""}`).join(`
|
|
2
2
|
`)}export function parseOptions(e){return String(e??"").split(`
|
|
3
3
|
`).filter(r=>r.trim()).map(r=>{const[n,...t]=r.split(":");return{value:n.trim(),label:t.join(":").trim()||n.trim()}})}const g=()=>({create:{enabled:!1,access:"admin"},read:{enabled:!0,access:"public"},update:{enabled:!1,access:"admin"},delete:{enabled:!1,access:"admin"}}),b=e=>({enabled:!0,access:e||"",formats:["csv","json"]});export function roleLadder(e){return(Array.isArray(e)?e:e?.entries||[]).map(n=>({name:n?.data?.name,label:n?.data?.label||n?.data?.name,level:Number(n?.data?.level)})).filter(n=>typeof n.name=="string"&&n.name&&Number.isFinite(n.level)).sort((n,t)=>n.level-t.level||n.name.localeCompare(t.name))}export function accessOptions(e,r,{token:n=!0}={}){const t=[{value:"public",label:"Public - anyone"}];n&&t.push({value:"token",label:"API token"});for(const l of[...e||[]].reverse())t.push({value:l.name,label:l.level===0?`${l.label} only`:`${l.label} and above`});return typeof r=="string"&&r&&!t.some(l=>l.value===r)&&t.push(e?.length?{value:r,label:`${r} (role not found)`,missing:!0}:{value:r,label:r}),t}let x=0;export const fieldKey=()=>`f${Date.now().toString(36)}${(x++).toString(36)}`;export function formFromCollection(e=null,{project:r="",defaultRole:n=""}={}){const t=e||{},l={...g(),...t.api||{}},o={...b(n),...t.export||{}},a={title:String(t.title??""),slug:String(t.slug??""),description:String(t.description??""),layout:t.layout==="grid"?"grid":"stacked",columns:String(Number.parseInt(t.columns,10)||2),project:String(t.meta?.project??r??""),bundled:t.bundled===!0,storageAdapter:t.storage?.adapter==="mongodb"?"mongodb":"file",storageConnection:String(t.storage?.connection||"default"),fields:(Array.isArray(t.fields)?t.fields:[]).map(i=>({...i,_key:fieldKey()})),readFields:Array.isArray(l.read?.fields)?l.read.fields.join(", "):"",exportEnabled:o.enabled!==!1,exportAccess:typeof o.access=="string"&&o.access?o.access:String(n||""),exportCsv:(Array.isArray(o.formats)?o.formats:EXPORT_FORMATS).includes("csv"),exportJson:(Array.isArray(o.formats)?o.formats:EXPORT_FORMATS).includes("json")};for(const i of OPERATIONS){a[`${i}Enabled`]=l[i]?.enabled===!0;const s=l[i]?.access;a[`${i}Access`]=typeof s=="string"&&s?s:"admin"}return a}export function cleanField(e){const{_key:r,...n}=e||{};return n}export function payloadFromForm(e,{isNew:r=!1,meta:n={}}={}){const t=e||{},l={};for(const i of OPERATIONS)l[i]={enabled:t[`${i}Enabled`]===!0,access:t[`${i}Access`]||"admin"};const o=String(t.readFields??"").split(",").map(i=>i.trim()).filter(Boolean);o.length&&(l.read.fields=o);const a=Math.min(6,Math.max(1,Number.parseInt(t.columns,10)||2));return{title:String(t.title??"").trim(),...r&&{slug:slugify(t.slug||t.title)},description:String(t.description??"").trim(),layout:t.layout==="grid"?"grid":"stacked",columns:a,fields:(t.fields||[]).map(cleanField),api:l,export:{enabled:t.exportEnabled!==!1,...t.exportAccess&&{access:t.exportAccess},formats:EXPORT_FORMATS.filter(i=>i==="csv"?t.exportCsv:t.exportJson)},storage:t.storageAdapter==="mongodb"?{adapter:"mongodb",connection:t.storageConnection||"default"}:{adapter:"file"},meta:{...n||{},project:String(t.project??"")||null},bundled:t.bundled===!0}}export function snapshot(e){const{fields:r=[],...n}=e||{};return JSON.stringify({...n,fields:r.map(cleanField)})}export function checkForm(e,{isNew:r=!1,taken:n=[],original:t={}}={}){const l=e||{},o={};if(String(l.title??"").trim()||(o.title="A collection needs a title."),r){const c=slugify(l.slug||l.title);c?n.includes(c)&&(o.slug=`"${c}" is already a collection.`):o.slug="The slug needs a letter or a number."}const a=Number.parseInt(l.columns,10);l.layout==="grid"&&!(a>=1&&a<=6)&&(o.columns="Between 1 and 6 columns.");const i={},s=new Map;return(l.fields||[]).forEach(c=>{const d=fieldProblem(c,t[c._key]);d&&(i[c._key]=d);const u=String(c.name??"").trim();u&&(s.has(u)?i[c._key]=i[c._key]||`"${u}" is used by another field.`:s.set(u,c._key))}),Object.keys(i).length&&(o.fields=i),l.exportEnabled&&!l.exportCsv&&!l.exportJson&&(o.export="Pick at least one export format, or turn export off."),o}export function fieldProblem(e,r){const n=String(e?.name??"").trim();return n?n!==r&&!FIELD_NAME_RE.test(n)?"Names start with a letter or _, then letters, digits, _ or -.":OPTION_TYPES.has(e.type)&&!(Array.isArray(e.options)&&e.options.length)?`A ${typeLabel(e.type).toLowerCase()} needs at least one option.`:"":"A field needs a name - it is what each entry is keyed by."}export const errorCount=e=>Object.keys(e||{}).filter(r=>r!=="fields").length+Object.keys(e?.fields||{}).length;export function fieldRow(e,r={},{grid:n=!1}={}){const t=[e.name||"(no name)",typeLabel(e.type)];return OPTION_TYPES.has(e.type)&&t.push(`${(e.options||[]).length} options`),n&&t.push(e.fullWidth?"full width":`span ${e.span>1?e.span:1}`),{key:e._key,label:e.label||"(Untitled field)",meta:t.join(" \xB7 "),required:e.required===!0,hidden:e.type==="hidden",problem:r[e._key]||"",rowCls:r[e._key]?"is-bad":""}}export function moveField(e,r,n){const t=[...e];if(r<0||r>=t.length||n<0||n>=t.length||r===n)return t;const[l]=t.splice(r,1);return t.splice(n,0,l),t}export function duplicateField(e,r){const n=e[r];if(!n)return[...e];const t=new Set(e.map(s=>s.name)),l=`${n.name||"field"}_copy`;let o=l;for(let s=2;t.has(o);s++)o=`${l}_${s}`;const a={...JSON.parse(JSON.stringify(cleanField(n))),name:o,label:`${n.label||"Field"} (copy)`,_key:fieldKey()};delete a.id;const i=[...e];return i.splice(r+1,0,a),i}export const newField=()=>({id:`field-${Date.now()}`,name:"",label:"",type:"string",required:!1,placeholder:"",helper:"",options:[],validation:[],logic:null,_key:fieldKey()});export function panelForm(e){return{label:String(e.label??""),name:String(e.name??""),type:e.type||"string",required:e.required===!0,placeholder:String(e.placeholder??""),helper:String(e.helper??""),options:optionsText(e.options),span:String(e.span>1?e.span:1),fullWidth:e.fullWidth===!0,prefixIcon:p(e.prefix).icon,prefixText:p(e.prefix).text,suffixIcon:p(e.suffix).icon,suffixText:p(e.suffix).text,clear:e.clear===!0,stepper:e.stepper===!0,counter:e.counter===!0}}export function fieldFromPanel(e,r){const n={...e,label:String(r.label??"").trim(),name:String(r.name??"").trim(),type:r.type||"string",required:r.required===!0,placeholder:String(r.placeholder??"").trim(),helper:String(r.helper??"").trim()};OPTION_TYPES.has(n.type)&&(n.options=parseOptions(r.options));for(const[t,l]of Object.entries(EXTRA_TYPES)){if(!l.has(n.type)){delete n[t];continue}t in r&&(r[t]===!0?n[t]=!0:delete n[t])}for(const t of["prefix","suffix"]){const l=`${t}Icon`in r||`${t}Text`in r;if(!ADDON_TYPES.has(n.type)){delete n[t];continue}if(!l)continue;const o=addonFromBoxes(r[`${t}Icon`],r[`${t}Text`]);o?n[t]=o:delete n[t]}if(r.fullWidth)n.fullWidth=!0,delete n.span;else{delete n.fullWidth;const t=Number.parseInt(r.span,10);t>1?n.span=Math.min(6,t):delete n.span}return n}export function storageChange(e,r){const n=e||"file",t=r||"file";return n===t?"":`${n} \u2192 ${t}`}export function draftOf(e){const{fields:r=[],...n}=e||{};return{...n,fields:r.map(cleanField)}}export function formFromDraft(e,r){const n=e||{};return{...r,...n,fields:(Array.isArray(n.fields)?n.fields:r.fields).map(t=>({...cleanField(t),_key:fieldKey()}))}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fmtDate as
|
|
1
|
+
import{fmtDate as y,fmtDateTime as d}from"./dates.js";export const OP_LABELS={eq:"=",ne:"\u2260",contains:"contains",gt:">",lt:"<"},MAX_COLUMNS=5,PAGE_SIZE=500,SHOW_STEP=100;export function matchesFilter(t,e){const r=t?.data?.[e.field],n=String(r??"");switch(e.op){case"eq":return n===String(e.value);case"ne":return n!==String(e.value);case"contains":return n.toLowerCase().includes(String(e.value).toLowerCase());case"gt":case"lt":{const o=s=>String(s).trim()!==""&&Number.isFinite(Number(s))?Number(s):NaN,i=o(n),c=o(e.value);if(!Number.isNaN(i)&&!Number.isNaN(c))return e.op==="gt"?i>c:i<c;const a=Date.parse(n),u=Date.parse(e.value);return!Number.isNaN(a)&&!Number.isNaN(u)?e.op==="gt"?a>u:a<u:e.op==="gt"?n>String(e.value):n<String(e.value)}default:return!0}}export const isFileRef=t=>!!t&&typeof t=="object"&&!Array.isArray(t)&&typeof t.url=="string"&&(typeof t.name=="string"||t.name===void 0),fileRefsOf=t=>(Array.isArray(t)?t:[t]).filter(isFileRef),isFileValue=t=>{const e=fileRefsOf(t);return e.length>0&&e.length===(Array.isArray(t)?t.length:1)};const b=t=>t.name||t.url.split("/").pop()||"file";export const safeFileHref=t=>/^(https?:\/\/|\/(?!\/))/i.test(String(t||""))?String(t):"";export function valueText(t){return t==null?"":typeof t=="boolean"?t?"Yes":"No":isFileRef(t)?b(t):Array.isArray(t)?t.map(e=>isFileRef(e)?b(e):e&&typeof e=="object"?JSON.stringify(e):String(e)).join(", "):typeof t=="object"?JSON.stringify(t):String(t)}export function shownValue(t,e){return e==null||e===""?e:t?.type==="date"?y(e):t?.type==="datetime"?d(e):e}export function cellText(t,e=60){const r=valueText(t).replace(/\s+/g," ").trim();return r.length>e?`${r.slice(0,e-1)}\u2026`:r}export function arrangeEntries(t,{filters:e=[],query:r="",sort:n={key:"created",dir:"desc"}}={}){const o=String(r||"").toLowerCase().trim();let i=(t||[]).filter(s=>e.every(p=>matchesFilter(s,p)));o&&(i=i.filter(s=>Object.values(s?.data||{}).some(p=>valueText(p).toLowerCase().includes(o))));const c=n?.dir==="asc"?1:-1,a=n?.key||"created",u=s=>a==="created"?String(s?.meta?.createdAt||""):s?.data?.[a];return[...i].sort((s,p)=>{const l=u(s),f=u(p),m=typeof l=="number"?l:parseFloat(l),g=typeof f=="number"?f:parseFloat(f);return a!=="created"&&!Number.isNaN(m)&&!Number.isNaN(g)&&String(m)===String(l??"").trim()&&String(g)===String(f??"").trim()?(m-g)*c:valueText(l).localeCompare(valueText(f),void 0,{numeric:!0,sensitivity:"base"})*c})}export function filterChoices(t,e){const r=valueText(e);if(r==="")return[];const n=valueText(shownValue(t,e)),o=n.length>28?`${n.slice(0,27)}\u2026`:n,i=[{op:"eq",value:r,label:`= \u201C${o}\u201D`},{op:"ne",value:r,label:`\u2260 \u201C${o}\u201D`}];return["number","date","time","datetime"].includes(t?.type)&&i.push({op:"gt",value:r,label:`> \u201C${o}\u201D`},{op:"lt",value:r,label:`< \u201C${o}\u201D`}),i}export function filterText(t){const e=String(t.value),r=/^\d{4}-\d{2}-\d{2}$/.test(e)?y(e):/^\d{4}-\d{2}-\d{2}T\d/.test(e)?d(e):e;return`${t.label} ${OP_LABELS[t.op]||t.op} \u201C${r.length>24?`${r.slice(0,23)}\u2026`:r}\u201D`}export function typedValue(t,e){if(t?.type==="number"&&typeof e=="string"&&e.trim()!==""){const r=Number(e);if(Number.isFinite(r))return r}return e}export function isSecretField(t){return t?.type==="password"||/pass(word)?|secret|token|api[-_]?key|private|credential/i.test(String(t?.name||""))}export function draftable(t,e=[]){const r=new Set(e.filter(isSecretField).map(n=>n.name));return Object.fromEntries(Object.entries(t||{}).filter(([n])=>!r.has(n)))}export function snapshot(t){const e=Object.keys(t||{}).sort();return JSON.stringify(e.map(r=>[r,typeof t[r]=="boolean"?t[r]:valueText(t[r])]))}export const hasForm=t=>!["roles","user-profiles"].includes(t),canClear=t=>t!=="roles";export function countText(t,e,r){const n=c=>c.toLocaleString("en-GB"),o=c=>`entr${c===1?"y":"ies"}`,i=t===e?`${n(e)} ${o(e)}`:`Showing ${n(t)} of ${n(e)}`;return r>e?`${i} \xB7 ${n(e)} of ${n(r)} loaded`:i}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const FILTERS=["inbox","new","spam"],WEEK_MS=6048e5;const g=new Set(["page-break","heading","paragraph","html","divider","spacer","banner"]);export function answerFields(e){return(e?.fields||[]).filter(t=>t&&t.name&&!g.has(t.type)).map(t=>({name:String(t.name),label:String(t.label||t.name),type:String(t.type||"text")}))}export function answerText(e){return e==null?"":Array.isArray(e)?e.map(answerText).filter(Boolean).join(", "):typeof e=="boolean"?e?"Yes":"No":typeof e=="object"?JSON.stringify(e):String(e).trim()}export const isSpam=e=>e?.data?.spam===!0,createdAt=e=>e?.meta?.createdAt||"",isNew=(e,t=Date.now())=>{const n=Date.parse(createdAt(e));return Number.isFinite(n)&&t-n<6048e5};const u=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;export function emailOf(e,t){const n=e?.data||{},
|
|
1
|
+
export const FILTERS=["inbox","new","spam"],WEEK_MS=6048e5;const g=new Set(["page-break","heading","paragraph","html","divider","spacer","banner"]);export function answerFields(e){return(e?.fields||[]).filter(t=>t&&t.name&&!g.has(t.type)).map(t=>({name:String(t.name),label:String(t.label||t.name),type:String(t.type||"text")}))}export const isFileRef=e=>!!e&&typeof e=="object"&&!Array.isArray(e)&&typeof e.url=="string"&&(typeof e.name=="string"||e.name===void 0),fileRefsOf=e=>(Array.isArray(e)?e:[e]).filter(isFileRef),safeFileHref=e=>/^(https?:\/\/|\/(?!\/))/i.test(String(e||""))?String(e):"";export function answerText(e){return e==null?"":Array.isArray(e)?e.map(answerText).filter(Boolean).join(", "):typeof e=="boolean"?e?"Yes":"No":isFileRef(e)?e.name||e.url.split("/").pop()||"file":typeof e=="object"?JSON.stringify(e):String(e).trim()}export const isSpam=e=>e?.data?.spam===!0,createdAt=e=>e?.meta?.createdAt||"",isNew=(e,t=Date.now())=>{const n=Date.parse(createdAt(e));return Number.isFinite(n)&&t-n<6048e5};const u=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;export function emailOf(e,t){const n=e?.data||{},a=t.find(r=>r.type==="email"&&u.test(answerText(n[r.name])));if(a)return answerText(n[a.name]);const i=t.find(r=>/e-?mail/i.test(r.name)&&u.test(answerText(n[r.name])));if(i)return answerText(n[i.name]);const c=t.find(r=>u.test(answerText(n[r.name])));return c?answerText(n[c.name]):""}const d=(e,t)=>e.length>t?`${e.slice(0,t-1)}\u2026`:e;export function submissionRow(e,t,n=Date.now()){const a=e?.data||{},i=t.filter(m=>answerText(a[m.name])!==""),c=i.find(m=>/(^|_|-)(full[-_ ]?)?name$|^name/i.test(m.name)&&m.type!=="email"),r=emailOf(e,t),o=c||i.find(m=>answerText(a[m.name])===r&&r)||i[0],p=o?d(answerText(a[o.name]),80):"Empty submission",s=i.filter(m=>m!==o).slice(0,3).map(m=>`${m.label}: ${d(answerText(a[m.name]),60)}`),l=isSpam(e),f=isNew(e,n);return{id:String(e?.id??""),title:p,meta:s.join(" \xB7 ")||"No other answers",email:r,at:createdAt(e),spam:l,fresh:f&&!l,rowCls:[l&&"is-spam",f&&!l&&"is-new"].filter(Boolean).join(" ")}}export function arrangeSubmissions(e,{filter:t="inbox",query:n="",from:a="",to:i="",now:c=Date.now()}={}){const r=String(n).trim().toLowerCase(),o=a?Date.parse(`${a}T00:00:00`):null,p=i?Date.parse(`${i}T23:59:59.999`):null;return[...e||[]].filter(s=>t==="spam"?isSpam(s):!isSpam(s)).filter(s=>t!=="new"||isNew(s,c)).filter(s=>!r||Object.values(s?.data||{}).some(l=>answerText(l).toLowerCase().includes(r))).filter(s=>{if(o===null&&p===null)return!0;const l=Date.parse(createdAt(s));return Number.isFinite(l)?(o===null||l>=o)&&(p===null||l<=p):!1}).sort((s,l)=>String(createdAt(l)).localeCompare(String(createdAt(s))))}export function submissionCounts(e,t=Date.now()){const n=e||[],a=n.filter(isSpam).length;return{inbox:n.length-a,new:n.filter(i=>!isSpam(i)&&isNew(i,t)).length,spam:a,all:n.length}}export function submissionText(e,t,{formTitle:n="",when:a=""}={}){const i=e?.data||{},c=t.filter(o=>answerText(i[o.name])!=="").map(o=>`${o.label}: ${answerText(i[o.name])}`),r=[n&&`${n} - submission`,a&&`Submitted ${a}`].filter(Boolean);return[...r,...r.length?[""]:[],...c].join(`
|
|
2
2
|
`)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fmtDate as u}from"./dates.js";export const KINDS=["all","image","video","audio","document","other"],KIND_LABEL={all:"All",image:"Images",video:"Video",audio:"Audio",document:"Documents",other:"Other"},SORTS=["newest","oldest","name","size"];const c={image:["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp","tif","tiff"],video:["mp4","webm","ogv","mov"],audio:["mp3","ogg","oga","wav","weba","m4a"],document:["pdf","txt","csv","doc","docx","xls","xlsx"]},
|
|
1
|
+
import{fmtDate as u}from"./dates.js";export const KINDS=["all","image","video","audio","document","other"],KIND_LABEL={all:"All",image:"Images",video:"Video",audio:"Audio",document:"Documents",other:"Other"},SORTS=["newest","oldest","name","size"];const c={image:["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp","tif","tiff"],video:["mp4","webm","ogv","mov"],audio:["mp3","ogg","oga","wav","weba","m4a"],document:["pdf","txt","csv","doc","docx","xls","xlsx"]},l=new Set(["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp"]),m=new Set(["jpg","jpeg","png","webp","gif","tiff"]);export const extOf=e=>{const t=/\.([a-z0-9]+)$/i.exec(String(e??""));return t?t[1].toLowerCase():""};export function kindOf(e){const t=extOf(e);for(const[n,r]of Object.entries(c))if(r.includes(t))return n;return"other"}export const isPreviewable=e=>l.has(extOf(e)),isEditable=e=>m.has(extOf(e));export function formatBytes(e){const t=Number(e)||0;if(t<1024)return`${t} B`;const n=["KB","MB","GB","TB"];let r=t/1024,o=0;for(;r>=1024&&o<n.length-1;)r/=1024,o++;return`${r.toFixed(1)} ${n[o]}`}export function ago(e,t=Date.now()){const n=new Date(e).getTime();if(!Number.isFinite(n))return"";const r=Math.max(0,Math.round((t-n)/1e3));if(r<60)return"just now";if(r<3600)return`${Math.round(r/60)} min ago`;if(r<86400){const o=Math.round(r/3600);return`${o} hour${o===1?"":"s"} ago`}if(r<86400*14){const o=Math.round(r/86400);return`${o} day${o===1?"":"s"} ago`}return u(n)}export function safeName(e){return String(e??"").split(/[\\/]/).pop().replace(/[^a-zA-Z0-9._-]/g,"_")}export function clashes(e,t){const n=new Set(t);return[...new Set(e.map(safeName))].filter(r=>n.has(r))}export function altFromName(e){return String(e??"").replace(/\.[^.]+$/,"").replace(/[-_]+/g," ").replace(/\s+/g," ").trim()}export function markdownFor(e){return isPreviewable(e.name)?``:`[${e.name}](${e.url})`}export function kindCounts(e){const t=Object.fromEntries(KINDS.map(n=>[n,0]));for(const n of e||[])t.all++,t[kindOf(n.name)]++;return t}export function arrangeMedia(e,{kind:t="all",query:n="",sort:r="newest"}={}){const o=String(n).trim().toLowerCase(),a={newest:(i,s)=>String(s.createdAt).localeCompare(String(i.createdAt)),oldest:(i,s)=>String(i.createdAt).localeCompare(String(s.createdAt)),name:(i,s)=>i.name.localeCompare(s.name,void 0,{numeric:!0,sensitivity:"base"}),size:(i,s)=>(s.size||0)-(i.size||0)}[r]||(()=>0);return[...e||[]].filter(i=>t==="all"||kindOf(i.name)===t).filter(i=>!o||i.name.toLowerCase().includes(o)).sort((i,s)=>a(i,s)||i.name.localeCompare(s.name))}export function mediaTile(e,{now:t=Date.now(),stamp:n=""}={}){const r=kindOf(e.name),o=isPreviewable(e.name);return{name:e.name,url:e.url,thumb:o?`${e.url}${n?`?t=${encodeURIComponent(n)}`:""}`:null,preview:o,icon:{video:"video",audio:"music",document:"file-text"}[r]||(r==="image"?"image":"paperclip"),kind:r,kindLabel:(extOf(e.name)||r).toUpperCase(),meta:[formatBytes(e.size),ago(e.createdAt,t)].filter(Boolean).join(" \xB7 ")}}export function normalisePrefs(e){const t=e&&typeof e=="object"?e:{};return{kind:KINDS.includes(t.kind)?t.kind:"all",sort:SORTS.includes(t.sort)?t.sort:"newest"}}export function normaliseLimits(e){const t=e&&typeof e=="object"?e:{},n=Array.isArray(t.types)?t.types.filter(a=>typeof a=="string"&&/^[\w.+-]+\/[\w.+-]+$/.test(a)):[],r=Number(t.maxFileSize),o=Number(t.maxNameLength);return{types:n,maxFileSize:Number.isFinite(r)&&r>0?r:null,maxNameLength:Number.isInteger(o)&&o>0?o:null}}export function dropzoneRules(e){const t=normaliseLimits(e);return{accept:t.types.length?t.types.join(","):null,maxSize:t.maxFileSize}}export function uploadHint(e){const t=normaliseLimits(e),n=[],r=a=>t.types.some(i=>a.test(i));r(/^image\//)&&n.push("images"),r(/^(application|text)\//)&&n.push("documents"),r(/^audio\//)&&n.push("audio"),r(/^video\//)&&n.push("video");const o=[];if(n.length){const a=n.length===1?n[0]:`${n.slice(0,-1).join(", ")} and ${n.at(-1)}`;o.push(a[0].toUpperCase()+a.slice(1))}return t.maxFileSize&&o.push(`up to ${formatBytes(t.maxFileSize)} each`),o.join(" \xB7 ")}export function nameRefusal(e,t){const n=normaliseLimits(t),r=safeName(e);return r?n.maxNameLength&&r.length>n.maxNameLength?`its name is longer than ${n.maxNameLength} characters`:null:"it has no name"}export function shortName(e,t=60){const n=String(e??"");return n.length>t?`${n.slice(0,t-15)}\u2026${n.slice(-12)}`:n}export function rejectText(e,t,n){const r=normaliseLimits(n),o=shortName(e);return t==="type"?`${o}: that type of file is not taken here`:t==="size"?`${o}: it is larger than ${r.maxFileSize?formatBytes(r.maxFileSize):"the limit"}`:`${o}: not added`}export function uploadErrorText(e,t){const n=e||{},r=n.response&&typeof n.response=="object"?n.response:null,o=r&&(typeof r.error=="string"?r.error:typeof r.message=="string"?r.message:"");if(n.status===413){const a=normaliseLimits(t);return`larger than ${a.maxFileSize?formatBytes(a.maxFileSize):"the server takes"}`}return o||(n.status===0?"the connection failed":n.status===401?"signed out - sign in and retry":n.status===403?"you may not upload media":n.message||"failed")}export function narrowLimits(e,t=null){const n=normaliseLimits(e);return t instanceof RegExp?{...n,types:n.types.filter(r=>t.test(r))}:n}export function uploadedFile(e){const t=Array.isArray(e)?e[0]:e;return!t||typeof t!="object"||typeof t.name!="string"||!t.name?null:{name:t.name,url:typeof t.url=="string"&&t.url?t.url:`/media/${t.name}`}}export function pickerOutcome({files:e=[],failed:t=0,waiting:n=0,offered:r=e.length,open:o=!0,single:a=!0}={}){return o?a&&e.length===1&&!t&&!n&&r<=1&&e[0]?.url?{choose:e[0].url,reload:!1}:{choose:null,reload:e.length>0}:{choose:null,reload:!1}}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as $}from"../api.js";import{onceClosed as A,sidebarCount as D}from"./tool-kit.js";import{catchFiles as N,mediaUploader as P}from"./media-upload.js";import{pickerOutcome as T}from"./media-arrange.js";const U=/\.(png|jpe?g|gif|webp|avif|svg)$/i;export const UPLOAD_TYPES=/^image\/(png|jpe?g|gif|webp|avif|svg\+xml)$/;export function pickable(s,{query:d="",accept:p=U}={}){const o=String(d).trim().toLowerCase();return(Array.isArray(s)?s:[]).map(t=>({name:t.name||t.filename||"",url:t.url||(t.name?`/media/${t.name}`:"")})).filter(t=>t.url&&p.test(t.name||t.url)&&(!o||t.name.toLowerCase().includes(o)))}export function pickMedia({title:s="Choose an image",current:d=""}={}){return new Promise(p=>{let o=!1;const t=e=>{o||(o=!0,p(e))},a=E.slideover({title:s,size:"lg",position:"right"}),c=document.createElement("template");c.innerHTML=`<div class="mp-picker">
|
|
2
2
|
<div class="mp-bar">
|
|
3
3
|
<input type="search" class="form-input" placeholder="Search Media\u2026" aria-label="Search Media" data-model="query.value">
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
</div>
|
|
5
|
+
<input class="mp-file-input" type="file" multiple hidden>
|
|
6
|
+
<div class="mp-zone">
|
|
7
|
+
<span class="mp-zone-icon" data-icon="upload" data-icon-size="18" aria-hidden="true"></span>
|
|
8
|
+
<span class="mp-zone-text"><b>Upload: drop pictures here, paste, or click to choose</b>
|
|
9
|
+
<span class="mp-zone-hint" data-bind-text="hint.value" data-bind-hidden="!hint.value"></span></span>
|
|
6
10
|
</div>
|
|
7
11
|
<p class="mp-note" data-bind-hidden="!note.value" data-bind-text="note.value"></p>
|
|
8
12
|
<div class="media-picker-grid mp-grid" data-each="shown.value key=url">
|
|
@@ -15,10 +19,18 @@ import{api as q}from"../api.js";import{onceClosed as S}from"./tool-kit.js";const
|
|
|
15
19
|
.mp-picker { padding: 1rem; display: flex; flex-direction: column; gap: .75rem; }
|
|
16
20
|
.mp-bar { display: flex; gap: .5rem; align-items: center; }
|
|
17
21
|
.mp-bar .form-input { flex: 1; }
|
|
18
|
-
|
|
22
|
+
/* A one-line strip rather than Domma's tall default area, as on the Media screen. */
|
|
23
|
+
.mp-zone.dm-dropzone { flex-direction: row; justify-content: flex-start; text-align: left; min-height: 0; gap: .65rem;
|
|
24
|
+
padding: .55rem .8rem; font-size: var(--dm-text-sm); }
|
|
25
|
+
.mp-picker.is-drop .mp-zone.dm-dropzone { border-color: var(--dm-primary); background: var(--dm-primary-light, transparent); }
|
|
26
|
+
.mp-zone-icon { color: var(--dm-primary); flex-shrink: 0; display: inline-flex; }
|
|
27
|
+
.mp-zone-text { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
|
|
28
|
+
.mp-zone-text b { font-weight: 600; }
|
|
29
|
+
.mp-zone-hint { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
|
|
30
|
+
.mp-picker .dm-dropzone-files { margin: 0; }
|
|
19
31
|
.mp-note { margin: 0; font-size: .85rem; opacity: .75; }
|
|
20
32
|
.mp-picker [hidden] { display: none !important; }
|
|
21
33
|
.mp-grid { max-height: none; }
|
|
22
34
|
.mp-grid .media-picker-item { background: none; padding: 0; text-align: left; color: inherit; font: inherit; }
|
|
23
35
|
.mp-grid .media-picker-item.is-current { border-color: var(--dm-primary); }
|
|
24
|
-
</style>`;const
|
|
36
|
+
</style>`;const i=c.content.firstElementChild,y=c.content.querySelector("style"),l=M.observable([]),m=M.observable(""),b=M.observable(""),r=M.observable("Loading\u2026"),u=M.computed(()=>pickable(l.value,{query:m.value})),z=M.effect(()=>{r.peek()==="Loading\u2026"&&!l.value.length||(r.value=u.value.length?"":m.value.trim()?"No picture matches.":"No pictures in Media yet - upload one.")}),x=async()=>{try{l.value=await $.media.list(),r.value="",pickable(l.peek()).length||(r.value="No pictures in Media yet - upload one.")}catch(e){r.value=`Could not load Media: ${e.message||e}`}},g=i.querySelector(".mp-grid"),k=e=>{const n=e.target.closest("[data-url]");n&&(t(n.dataset.url),a.close())};g.addEventListener("click",k),a.element.appendChild(i),y&&a.element.appendChild(y);const f=()=>!o,h=P(i.querySelector(".mp-zone"),{input:i.querySelector(".mp-file-input"),accept:"image/*",only:UPLOAD_TYPES,exists:e=>l.peek().some(n=>(n.name||n.filename)===e),showing:f,onHint:e=>{b.value=e},doneSuffix:()=>f()?"":" to Media",onUploaded({files:e,failed:n,waiting:C,offered:L}){D("#/media");const v=T({files:e,failed:n,waiting:C,offered:L,open:f()});if(v.choose){t(v.choose),setTimeout(()=>a.close(),0);return}v.reload&&x()}}),w=N(a.element,h.addFiles,{onDragging:e=>i.classList.toggle("is-drop",e)}),S=M.applyBindings({query:m,note:r,shown:u,hint:b},i),q=M.effect(()=>{u.value,queueMicrotask(()=>g.querySelectorAll("[data-url]").forEach(e=>e.classList.toggle("is-current",!!d&&e.dataset.url===d)))});A(a,()=>{const e=h.pending();S.dispose(),z?.(),q?.(),w(),g.removeEventListener("click",k),t(null),h.release()&&e&&E.toast(`Still uploading ${e} file${e===1?"":"s"} - ${e===1?"it goes":"they go"} into Media when done.`,{type:"info"})}),Domma.icons?.scan?.(i),a.open(),x(),setTimeout(()=>i.querySelector("input[type=search]")?.focus(),50)})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{api as M,getToken as V,refreshAccessToken as W}from"../api.js";import{dropzoneRules as G,nameRefusal as I,narrowLimits as P,rejectText as J,safeName as v,shortName as _,uploadErrorText as K,uploadHint as Q,uploadedFile as X}from"./media-arrange.js";const Y=f=>String(f??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),U=f=>setTimeout(f,0);export function mediaUploader(f,{input:$,multiple:h=!0,accept:p=null,only:d=null,exists:T=()=>!1,showing:w=()=>!0,onHint:L=()=>{},onUploaded:j=()=>{},doneSuffix:q=()=>""}={}){let l=P(null,d);const t=new WeakSet;let o=[],m=[],b=[],y=0,i=!1,g=0,x=0,R=0,k=!1,c=null;const B=()=>{if(!m.length)return;const e=m;m=[],E.toast(`Not added - ${e.slice(0,4).join("; ")}${e.length>4?` and ${e.length-4} more`:""}`,{type:"error",duration:9e3})},F=e=>{m.length||U(B),m.push(e)};async function C(){const e=o;if(o=[],!(!e.length||!w())){x=e.length;try{await H(e)}finally{x=0}}}async function H(e){const n=[...new Set(e.map(a=>v(a.name)))],u=n.slice(0,5).map(a=>`"${Y(a)}"`).join(", ")+(n.length>5?` and ${n.length-5} more`:""),s=n.length===1;await E.confirm(`${s?"A file":`${n.length} files`} with the same name ${s?"is":"are"} already here: ${u}. Replace ${s?"it":"them"}?`)&&(i||c||(e.forEach(a=>t.add(a)),r.addFiles(e)))}const r=E.dropzone(f,{input:$,multiple:h,previews:!0,accept:p,upload:{url:"/api/media",field:"file",concurrency:3,headers:()=>{const e=V();return e?{Authorization:`Bearer ${e}`}:{}}},labels:{cancel:"Cancel the upload of {name}",remove:"Remove {name} from the list"},onBeforeAdd(e){const n=I(e.name,l);if(n)return F(`${_(e.name)}: ${n}`),!1;if(t.has(e.file))return t.delete(e.file),!0;const u=v(e.name),s=r.getEntries().filter(a=>v(a.name)===u);return s.some(a=>a.status==="uploading"||a.status==="queued")?(F(`${_(e.name)}: a file of that name is uploading already`),!1):(s.forEach(a=>{a.status!=="done"&&r.removeFile(a.id)}),k||(k=!0,queueMicrotask(()=>{k=!1}),!O()&&!r.getEntries().some(a=>a.status!=="done")&&(R=0)),R++,T(u)?(o.length||U(C),o.push(e.file),!1):!0)},onReject(e,n){F(J(e.name,n,l))},onSuccess(e,n){b.push(X(n)||{name:v(e.name),url:`/media/${v(e.name)}`})},onError(e,n){if(n.status===401&&!e._mdxRefreshed&&S.get("auth_refresh_token")){e._mdxRefreshed=!0,n.message="signing in again\u2026",g++,W().then(()=>{i||r.retry(e.id)}).catch(()=>{}).finally(()=>{g--,c?.()});return}y++,n.message=K(n,l)},onComplete(){const e=b,n=y;b=[],y=0;const u=e.length;if(u&&E.toast(`Uploaded ${u} file${u===1?"":"s"}${q()}.`,{type:"success"}),n){const s=`${n} file${n===1?"":"s"} not uploaded`;E.toast(w()?`${s} - Retry or remove ${n===1?"it":"them"} in the list.`:`${s}.`,{type:"error",duration:9e3})}r.getEntries().filter(s=>s.status==="done").forEach(s=>r.removeFile(s.id)),u&&j({files:e,failed:n,waiting:O(),offered:R})}});function O(){return o.length+x+g}const z=()=>!i&&r.getEntries().some(e=>e.status==="uploading"||e.status==="queued"),D=()=>z()||g>0,A=e=>{D()&&(e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",A);const N=()=>{i||(i=!0,window.removeEventListener("beforeunload",A),r.destroy())};return M.media.limits().then(e=>{if(i||c||!w())return;l=P(e,d);const n=G(l);r.setOptions({...n,accept:n.accept??p}),L(Q(l))}).catch(()=>{}),{dz:r,addFiles:e=>{!i&&!c&&r.addFiles(e)},browse:()=>{!i&&!c&&r.browse()},uploading:z,pending:()=>i?0:r.getEntries().filter(e=>e.status==="uploading"||e.status==="queued").length+g,release(){return i||c?!1:(c=()=>{D()||N()},D()?(r.on("complete",c),!0):(N(),!1))}}}export function catchFiles(f,$,{onDragging:h=()=>{}}={}){const p=t=>[...t.dataTransfer?.types||[]].includes("Files");let d=0;const l={dragenter:t=>{p(t)&&(t.preventDefault(),d++,h(!0))},dragover:t=>{p(t)&&(t.preventDefault(),t.dataTransfer.dropEffect="copy")},dragleave:t=>{p(t)&&(d=Math.max(0,d-1),d||h(!1))},drop:t=>{p(t)&&(d=0,h(!1),!t.defaultPrevented&&(t.preventDefault(),$(t.dataTransfer.files)))},paste:t=>{const o=[...t.clipboardData?.files||[]];t.defaultPrevented||!o.length||t.target?.closest?.('input, textarea, [contenteditable=""], [contenteditable="true"]')&&[...t.clipboardData.types||[]].includes("text/plain")||(t.preventDefault(),$(o))}};return Object.entries(l).forEach(([t,o])=>f.addEventListener(t,o)),()=>Object.entries(l).forEach(([t,o])=>f.removeEventListener(t,o))}
|
|
@@ -97,6 +97,10 @@
|
|
|
97
97
|
.cex-detail { display: flex; flex-direction: column; gap: .6rem; }
|
|
98
98
|
.cex-detail dt { font-size: var(--dm-text-xs); color: var(--dm-text-muted); font-weight: 600; }
|
|
99
99
|
.cex-detail dd { margin: 0 0 .5rem; word-break: break-word; white-space: pre-wrap; padding-bottom: .5rem; border-bottom: 1px solid var(--dm-border); }
|
|
100
|
+
.cex-files { margin: 0; padding: 0; list-style: none; white-space: normal; display: grid; gap: .2rem; }
|
|
101
|
+
.cex-files a { color: var(--dm-primary); text-decoration: underline; }
|
|
102
|
+
.cex-files-field { display: grid; gap: .25rem; margin-bottom: .75rem; }
|
|
103
|
+
.cex-files-field > span { font-weight: 600; font-size: .875rem; }
|
|
100
104
|
.cex-meta { font-size: var(--dm-text-xs); color: var(--dm-text-muted); display: flex; flex-direction: column; gap: .2rem; }
|
|
101
105
|
.cex-import { width: 100%; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); min-height: 12rem; resize: vertical; }
|
|
102
106
|
.cex-export { display: flex; flex-direction: column; gap: .5rem; }
|
|
@@ -73,6 +73,9 @@
|
|
|
73
73
|
.fsx-answer { padding: .55rem 0; border-bottom: 1px solid var(--dm-border); }
|
|
74
74
|
.fsx-answer dt { font-size: var(--dm-text-xs); font-weight: 600; color: var(--dm-text-muted); margin-bottom: .15rem; }
|
|
75
75
|
.fsx-answer dd { margin: 0; white-space: pre-wrap; word-break: break-word; }
|
|
76
|
+
.fsx-files { margin: 0; padding: 0; list-style: none; white-space: normal; display: grid; gap: .2rem; }
|
|
77
|
+
.fsx-files small { color: var(--dm-text-muted); }
|
|
78
|
+
.fsx-files a { color: var(--dm-primary); text-decoration: underline; }
|
|
76
79
|
.fsx-facts { display: flex; flex-wrap: wrap; gap: .35rem .9rem; color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
|
|
77
80
|
.fsx-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
|
|
78
81
|
.fsx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as k,apiRequest as pt}from"../api.js";import{addBannerAction as j}from"../lib/plugin-chrome.js";import{SHAPES as ft,showLoading as mt}from"../lib/loading.js";import{helpPopovers as ze}from"../lib/help-popover.js";import{onceClosed as ht,openMenuAt as je,sidebarCount as P,viewLifetime as yt}from"../lib/tool-kit.js";import{MAX_COLUMNS as vt,PAGE_SIZE as ce,SHOW_STEP as X,arrangeEntries as bt,canClear as gt,cellText as Pe,countText as Et,draftable as kt,fileRefsOf as xt,filterChoices as wt,filterText as St,hasForm as Ct,isFileValue as ve,safeFileHref as $t,shownValue as be,snapshot as _e,typedValue as At,valueText as ge}from"../lib/collection-entries-arrange.js";import{fmtDate as Mt,fmtDateTime as _}from"../lib/dates.js";const Be=/^#\/collections\/[^/?#]+\/entries\/?(\?.*)?$/,B=p=>`collection-entries.draft.${p}`,U=p=>String(p??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function le(p){const y=document.createElement("template");return y.innerHTML=p.trim(),y.content.firstElementChild}const Lt='<span class="cex-chip">{{text}}<button type="button" data-idx="{{idx}}" title="Remove this filter" aria-label="Remove filter {{text}}">\xD7</button></span>';function qt(p){return`
|
|
2
2
|
<tr class="cex-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls">
|
|
3
3
|
<td class="cex-check"><input type="checkbox" class="cex-sel" aria-label="Select this entry" data-bind-checked="sel"></td>
|
|
4
|
-
${Array.from({length:
|
|
4
|
+
${Array.from({length:p},(b,a)=>`<td data-col="${a}" title="{{t${a}}}">{{c${a}}}</td>`).join("")}
|
|
5
5
|
<td class="cex-when" title="{{whenFull}}">{{when}}</td>
|
|
6
6
|
<td class="cex-tools"><button type="button" class="cex-run" data-bind-hidden="!runs" title="Run one of this collection's actions">\u26A1 {{runs}}</button><button type="button" class="cex-kebab" aria-label="Actions for this entry" title="Actions">\u22EE</button></td>
|
|
7
|
-
</tr>`}function
|
|
7
|
+
</tr>`}const Tt=(p,y)=>p?.type==="file"||ve(y?.[p?.name]);function He(p){const y=document.createElement("ul");y.className="cex-files";for(const b of xt(p)){const a=document.createElement("li"),c=$t(b.url),l=document.createElement(c?"a":"span");c&&(l.href=c,l.target="_blank",l.rel="noopener"),l.textContent=ge(b),a.appendChild(l),y.appendChild(a)}return y}function Nt(p,y){const b={};return p.forEach(a=>{const c={};a.fullWidth?c.span=y:a.span>1&&(c.span=a.span),a.placeholder&&(c.placeholder=a.placeholder),a.helper&&(c.hint=a.helper),a.prefix&&(c.prefix=a.prefix),a.suffix&&(c.suffix=a.suffix);for(const u of["clear","stepper","counter"])a[u]===!0&&(c[u]=!0);const l=a.type==="checkbox"?"boolean":a.type==="date"?"string":a.type;b[a.name]={type:l,label:a.label,required:a.required,options:a.options,...Object.keys(c).length?{formConfig:c}:{}}}),b}function ie(p,y){const b={};for(const a of y){const c=[...p.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!c.length)continue;const l=c[0];l.type==="radio"?b[a.name]=c.find(u=>u.checked)?.value??"":l.type==="checkbox"&&c.length>1?b[a.name]=c.filter(u=>u.checked).map(u=>u.value):l.type==="checkbox"?b[a.name]=l.checked:l.tagName==="SELECT"&&l.multiple?b[a.name]=[...l.selectedOptions].map(u=>u.value):b[a.name]=At(a,l.value)}return b}function Ve(p,y,b){for(const a of y){if(!(a.name in b))continue;const c=b[a.name],l=[...p.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!l.length)continue;const u=l[0];u.type==="radio"?l.forEach(r=>{r.checked=r.value===String(c)}):u.type==="checkbox"&&l.length>1?l.forEach(r=>{r.checked=Array.isArray(c)&&c.includes(r.value)}):u.type==="checkbox"?u.checked=c===!0:u.tagName==="SELECT"&&u.multiple?[...u.options].forEach(r=>{r.selected=Array.isArray(c)&&c.includes(r.value)}):u.value=c??"",l.forEach(r=>r.dispatchEvent(new Event("input",{bubbles:!0})))}}const Je=yt(Be);export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(p){Je.reset();const y=p.get(0).querySelector(".cex")||p.get(0),b=location.hash.match(/^#\/collections\/([^/?#]+)\/entries/),a=b?decodeURIComponent(b[1]):null,c=()=>Be.test(location.hash)&&y.isConnected&&location.hash.includes(`/collections/${encodeURIComponent(a)}/entries`),l=e=>(Je.add(e),e);if(!a){E.toast("No collection selected.",{type:"error"});return}const u=M.observable(null),r=M.observable([]),x=M.observable(0),Y=M.observable(!0),H=M.observable(""),Ke=M.computed(()=>u.value!==null),re=M.observable(""),w=M.observable([]),Z=M.observable({key:"created",dir:"desc"}),g=M.observable([]),N=M.observable(X),V=M.observable([]),Q=M.observable({}),L=M.observable(!1);let ee=1;const te=()=>u.peek()?.fields||[],de=M.computed(()=>(u.value?.fields||[]).slice(0,vt)),J=e=>r.peek().find(t=>t.id===e),Ee=e=>V.peek().filter(t=>{const n=Q.peek()[t.slug];return n==null||n.has(e)}),D=M.observable([]),ke=M.observable([]);l(M.effect(()=>{const e=bt(r.value,{filters:w.value,query:re.value,sort:Z.value});D.value=e})),l(M.effect(()=>{const e=de.value,t=new Set(g.value);Q.value,V.value,ke.value=D.value.slice(0,N.value).map(n=>{const o={id:n.id,sel:t.has(n.id),rowCls:t.has(n.id)?"is-selected":""};e.forEach((i,m)=>{const d=be(i,n.data?.[i.name]);o[`c${m}`]=Pe(d),o[`t${m}`]=ge(d)});const s=n.meta?.createdAt;o.when=s?Mt(s):"",o.whenFull=s?`Created ${_(s)}${n.meta?.updatedAt?` \xB7 changed ${_(n.meta.updatedAt)}`:""}`:"";const f=Ee(n.id).length;return o.runs=f?String(f):"",o})}));const Ge=M.computed(()=>w.value.map((e,t)=>({key:`${t}:${e.field}:${e.op}:${e.value}`,idx:t,text:St(e)}))),We=M.computed(()=>g.value.length),Xe={schema:u,loaded:r,loading:Y,loadError:H,ready:Ke,query:re,rows:ke,chips:Ge,busy:L,selectedCount:We,countLine:M.computed(()=>Et(D.value.length,r.value.length,x.value)),selectedText:M.computed(()=>`${g.value.length} selected`),canClear:M.computed(()=>gt(a)),introText:M.computed(()=>{const e=u.value;return e?`${e.description?`${e.description} - `:""}${(e.fields||[]).length} field${(e.fields||[]).length===1?"":"s"}${e.storage?.adapter==="mongodb"?", on MongoDB":""}.`:""}),emptyText:M.computed(()=>r.value.length?"No entries match the search or filters.":"No entries yet - Add entry in the banner makes one, or Import brings many."),moreText:M.computed(()=>{const e=D.value.length-N.value;if(e>0)return`Show ${Math.min(X,e)} more (${e} not shown)`;const t=x.value-r.value.length;return t>0?`Load the next ${Math.min(ce,t)} from the server`:""})};async function K({reset:e=!0,pending:t=null}={}){Y.value=!0;try{const n=e?Math.max(ce,r.peek().length):ce;e&&(ee=1);const o=await k.collections.listEntries(a,{limit:n,page:ee});if(t&&await t.done(),!c())return;const s=o?.entries||(Array.isArray(o)?o:[]);r.value=e?s:[...r.peek(),...s.filter(i=>!J(i.id))],x.value=Number(o?.total)||r.peek().length,e&&(ee=Math.max(1,Math.ceil(r.peek().length/ce)));const f=new Set(r.peek().map(i=>i.id));g.value=g.peek().filter(i=>f.has(i)),H.value="",await Qe()}catch(n){H.value=`Could not load the entries: ${n.message||n}`}finally{t&&await t.done(),Y.value=!1}}async function Ye(){ee+=1,await K({reset:!1})}async function Ze(){try{if((await pt("/actions/_count",{method:"GET"}).catch(()=>null))?.available===!1)return;const t=await k.actions.forCollection(a);V.value=Array.isArray(t)?t.filter(n=>n.canRun!==!1):[]}catch{V.value=[]}}async function Qe(){const e=V.peek().filter(o=>o.access?.rowLevel);if(!e.length||!r.peek().length)return;const t=r.peek().map(o=>o.id),n={...Q.peek()};await Promise.all(e.map(async o=>{try{n[o.slug]=new Set((await k.actions.checkAccess(o.slug,t)).allowed)}catch{n[o.slug]=null}})),Q.value=n}const G=e=>{const t=te()[0];return(t?Pe(be(t,e?.data?.[t.name]),40):"")||`Entry ${String(e?.id||"").slice(0,8)}`};async function xe(e){if(await E.confirm(`Delete \u201C${U(G(e))}\u201D? This cannot be undone.`))try{await k.collections.deleteEntry(a,e.id),r.value=r.peek().filter(t=>t.id!==e.id),x.value=Math.max(0,x.peek()-1),g.value=g.peek().filter(t=>t!==e.id),P("#/collections"),E.toast("Entry deleted.",{type:"success"})}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function et(){const e=[...g.peek()];if(!e.length||!await E.confirm(`Delete ${e.length} entr${e.length===1?"y":"ies"}? This cannot be undone.`))return;L.value=!0;let t=0;const n=[];for(const o of e)try{await k.collections.deleteEntry(a,o),t++}catch(s){n.push(s.message||String(s))}L.value=!1,g.value=[],P("#/collections"),E.toast(n.length?`Deleted ${t}; ${n.length} refused (${n[0]}).`:`Deleted ${t} entr${t===1?"y":"ies"}.`,{type:n.length?"warning":"success",duration:n.length?7e3:4e3}),c()&&await K()}async function tt(){const e=u.peek();if(await E.confirm(`Delete ALL ${x.peek()} entries in \u201C${U(e?.title||a)}\u201D? This cannot be undone.`)){L.value=!0;try{await k.collections.clearEntries(a),r.value=[],x.value=0,g.value=[],P("#/collections"),E.toast("All entries deleted.",{type:"success"})}catch(t){E.toast(`Not cleared: ${t.message||t}`,{type:"error"})}finally{L.value=!1}}}async function nt(e){try{const t=await k.collections.createEntry(a,{...e.data||{}});r.value=[t,...r.peek()],x.value=x.peek()+1,P("#/collections"),E.toast("Duplicated - the copy is at the top.",{type:"success"})}catch(t){E.toast(`Not duplicated: ${t.message||t}`,{type:"error"})}}async function at(e,t){const n=e.trigger?.confirmMessage;if(!(n&&!await E.confirm(U(n)))){L.value=!0;try{const o=await k.actions.execute(e.slug,t.id);if(o?.success){const s=o.stepsCompleted??0;E.toast(`${e.trigger?.label||e.title||"Action"} done (${s} step${s===1?"":"s"}).`,{type:"success"}),c()&&await K()}else E.toast(o?.results?.find(s=>!s.success)?.error||"The action failed.",{type:"error"})}catch(o){E.toast(o.message||"The action failed.",{type:"error"})}finally{L.value=!1}}}async function we(e){try{await navigator.clipboard.writeText(e),E.toast("Copied.",{type:"success",duration:1500})}catch{E.toast("Copy failed - your browser refused the clipboard.",{type:"error"})}}function ue(e){const t=g.peek();g.value=t.includes(e)?t.filter(n=>n!==e):[...t,e]}function Se(e,t,n){w.value=[...w.peek(),{field:e.name,label:e.label||e.name,op:t,value:n}],N.value=X}const pe=new Set;let O=null,Ce=!1;function $e(e,t,n){const o=e.close.bind(e);let s=!1,f=!1;e.close=async i=>{if(!s&&i!==!0&&!Ce&&t()){if(f)return;f=!0;const m=await E.confirm(n);if(f=!1,!m)return}return s=!0,o()}}function ne(e,t=()=>{}){pe.add(e),ht(e,()=>{pe.delete(e),t()})}function z(e,t=null){const n=u.peek(),o=te().filter(h=>Tt(h,e?.data)),s=te().filter(h=>!o.includes(h)),f=Object.fromEntries(o.filter(h=>e?.data&&h.name in e.data).map(h=>[h.name,e.data[h.name]])),i=!!e,m=E.slideover({title:i?`Edit ${U(G(e))}`:"New entry",size:"md",position:"right"}),d=le(`
|
|
8
8
|
<div class="cex-form">
|
|
9
9
|
<div class="cex-fields"></div>
|
|
10
10
|
<small class="cex-hint cex-when-line"></small>
|
|
@@ -15,7 +15,7 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
|
|
|
15
15
|
<button type="button" class="btn btn-sm btn-ghost" data-act="cancel">Cancel</button>
|
|
16
16
|
<button type="button" class="btn btn-sm btn-primary" data-act="save"><span data-icon="check" data-icon-size="13"></span> <span class="cex-save-text"></span></button>
|
|
17
17
|
</div>
|
|
18
|
-
</div>`);
|
|
18
|
+
</div>`);d.querySelector(".cex-save-text").textContent=i?"Save":"Add entry";const v=d.querySelector(".cex-fields");for(const h of o){if(!ve(f[h.name]))continue;const $=document.createElement("div");$.className="cex-files-field";const T=document.createElement("span");T.textContent=h.label||h.name,$.append(T,He(f[h.name])),v.before($)}F.create(Nt(s,n?.columns||2),e?.data||{},{layout:n?.layout||"stacked",columns:n?.columns||2,showSubmitButton:!1}).renderTo(v),s.forEach(h=>{if(h.type!=="date"||!h.name)return;const $=v.querySelector(`[name="${CSS.escape(h.name)}"]`);$&&$.type!=="date"&&($.type="date")}),e?.data&&Ve(v,s,e.data),i&&e.meta?.createdAt&&(d.querySelector(".cex-when-line").textContent=`Created ${_(e.meta.createdAt)}${e.meta.updatedAt?`, last changed ${_(e.meta.updatedAt)}`:""}${e.meta.source?` \xB7 via ${e.meta.source}`:""}.`),m.element.appendChild(d);const dt=_e(ie(v,s));t&&Ve(v,s,t);const me=()=>_e(ie(v,s))!==dt,ut=d.querySelector(".cex-dirty"),he=()=>{ut.hidden=!me()};v.addEventListener("input",he),v.addEventListener("change",he),he();let ye=!1;async function Re(){if(ye)return;document.activeElement?.dispatchEvent?.(new Event("change",{bubbles:!0}));const h={...ie(v,s),...f};ye=!0;const $=d.querySelector('[data-act="save"]');$.disabled=!0;try{if(i){const T=await k.collections.updateEntry(a,e.id,h);r.value=r.peek().map(Ue=>Ue.id===e.id?T:Ue),E.toast("Entry saved.",{type:"success"})}else{const T=await k.collections.createEntry(a,h);r.value=[T,...r.peek()],x.value=x.peek()+1,P("#/collections"),E.toast("Entry added.",{type:"success"})}try{S.remove?S.remove(B(a)):S.set(B(a),null)}catch{}m.close(!0)}catch(T){E.toast(T.message||"Not saved.",{type:"error"})}finally{ye=!1,$.disabled=!1}}d.querySelector('[data-act="save"]').addEventListener("click",Re),d.querySelector('[data-act="cancel"]').addEventListener("click",()=>m.close()),d.addEventListener("keydown",h=>{(h.ctrlKey||h.metaKey)&&(h.key==="Enter"||h.key==="s")&&(h.preventDefault(),Re())});const Fe={id:e?.id??null,dirty:me,values:()=>kt(ie(v,s),s)};O=Fe,ne(m,()=>{O===Fe&&(O=null)}),$e(m,me,"Discard the changes you have not saved?"),I.scan(d),m.open(),v.querySelector("input, select, textarea")?.focus()}function st(e){const t=E.slideover({title:U(G(e)),size:"md",position:"right"}),n=le(`
|
|
19
19
|
<div class="cex-form">
|
|
20
20
|
<dl class="cex-detail"></dl>
|
|
21
21
|
<div class="cex-meta"></div>
|
|
@@ -24,7 +24,7 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
|
|
|
24
24
|
<span class="cex-spacer"></span>
|
|
25
25
|
<button type="button" class="btn btn-sm btn-primary" data-act="edit"><span data-icon="edit" data-icon-size="13"></span> Edit</button>
|
|
26
26
|
</div>
|
|
27
|
-
</div>`),
|
|
27
|
+
</div>`),o=n.querySelector(".cex-detail");for(const i of te()){const m=ge(be(i,e.data?.[i.name])),d=document.createElement("dt");d.textContent=i.label||i.name;const v=document.createElement("dd");ve(e.data?.[i.name])?v.appendChild(He(e.data[i.name])):v.textContent=m===""?"-":m,o.append(d,v)}const s=n.querySelector(".cex-meta"),f=i=>{const m=document.createElement("span");m.textContent=i,s.appendChild(m)};f(`Id: ${e.id}`),e.meta?.createdAt&&f(`Created: ${_(e.meta.createdAt)}`),e.meta?.updatedAt&&f(`Changed: ${_(e.meta.updatedAt)}`),e.meta?.source&&f(`Source: ${e.meta.source}`),n.querySelector('[data-act="copy"]').addEventListener("click",()=>we(JSON.stringify(e,null,2))),n.querySelector('[data-act="edit"]').addEventListener("click",()=>{t.close(!0),z(e)}),t.element.appendChild(n),ne(t),I.scan(n),t.open()}function ot(){const e=E.slideover({title:"Import entries",size:"md",position:"right"}),t=le(`
|
|
28
28
|
<div class="cex-form">
|
|
29
29
|
<p class="cex-hint">A JSON array of entries, each with a "data" object of field values - what Export gives you.
|
|
30
30
|
<span data-help="Each entry is added as new, with a new id and today's date (the Data Transfer plugin from the Marketplace keeps ids and dates). Entries are checked like any other save - one that fails, or points a reference at an entry that does not exist, is skipped and reported. Values are stored as given, including fields the collection does not have."></span></p>
|
|
@@ -40,11 +40,11 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
|
|
|
40
40
|
<button type="button" class="btn btn-sm btn-ghost" data-act="cancel">Cancel</button>
|
|
41
41
|
<button type="button" class="btn btn-sm btn-primary" data-act="import"><span data-icon="upload" data-icon-size="13"></span> Import</button>
|
|
42
42
|
</div>
|
|
43
|
-
</div>`),n=t.querySelector("textarea"),
|
|
43
|
+
</div>`),n=t.querySelector("textarea"),o=t.querySelector(".cex-err"),s=t.querySelector(".cex-ok"),f=t.querySelector('input[type="file"]');t.querySelector('[data-act="file"]').addEventListener("click",()=>f.click()),f.addEventListener("change",async()=>{const m=f.files?.[0];f.value="",m&&(n.value=await m.text())}),t.querySelector('[data-act="cancel"]').addEventListener("click",()=>e.close());let i=!1;t.querySelector('[data-act="import"]').addEventListener("click",async()=>{o.hidden=!0,s.hidden=!0;let m;try{if(m=JSON.parse(n.value),!Array.isArray(m))throw new Error("it must be a JSON array")}catch(v){o.textContent=`Not valid: ${v.message}`,o.hidden=!1;return}const d=t.querySelector('[data-act="import"]');d.disabled=!0;try{const v=await k.collections.import(a,m);s.textContent=`Imported ${v.imported}; skipped ${v.skipped}.`,s.hidden=!1,i=!0,P("#/collections"),c()&&await K(),setTimeout(()=>e.close(!0),1200)}catch(v){o.textContent=v.message||"Import failed.",o.hidden=!1}finally{d.disabled=!1}}),e.element.appendChild(t),ne(e),$e(e,()=>!i&&n.value.trim()!=="","Discard the entries you pasted?"),ze(t),I.scan(t),e.open(),n.focus()}function ct(){const e=E.slideover({title:"Export entries",size:"sm",position:"right"}),t=le(`
|
|
44
44
|
<div class="cex-form">
|
|
45
45
|
<p class="cex-hint">Every entry in the collection - not only the ones shown.</p>
|
|
46
46
|
<div class="cex-export">
|
|
47
47
|
<button type="button" class="btn btn-ghost" data-fmt="json"><span data-icon="download" data-icon-size="14"></span> Export as JSON</button>
|
|
48
48
|
<button type="button" class="btn btn-ghost" data-fmt="csv"><span data-icon="download" data-icon-size="14"></span> Export as CSV</button>
|
|
49
49
|
</div>
|
|
50
|
-
</div>`);t.querySelectorAll("[data-fmt]").forEach(n=>n.addEventListener("click",async()=>{const
|
|
50
|
+
</div>`);t.querySelectorAll("[data-fmt]").forEach(n=>n.addEventListener("click",async()=>{const o=n.dataset.fmt;try{const s=await fetch(`/api/collections/${encodeURIComponent(a)}/export?format=${o}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!s.ok)throw new Error(`${s.status}`);const f=await s.blob(),i=document.createElement("a");i.href=URL.createObjectURL(f),i.download=`${a}-entries.${o}`,document.body.appendChild(i),i.click(),i.remove(),setTimeout(()=>URL.revokeObjectURL(i.href),1e3),e.close(!0)}catch(s){E.toast(`Export failed (${s.message}).`,{type:"error"})}})),e.element.appendChild(t),ne(e),I.scan(t),e.open()}function lt(e,t){const n=J(e.dataset.id);if(!n)return[];const o=t?.event?.target?.closest?.("td[data-col]"),s=o?de.peek()[Number(o.dataset.col)]:null,f=Ee(n.id),i=g.peek().includes(n.id),m=s?wt(s,n.data?.[s.name]):[];return[{header:G(n)},{label:"Edit\u2026",icon:"edit",action:()=>z(n)},{label:"See all of it\u2026",icon:"eye",action:()=>st(n)},{label:"Duplicate",icon:"copy",action:()=>nt(n)},{label:"Copy id",icon:"hash",action:()=>we(n.id)},{label:i?"Deselect":"Select",icon:"check",action:()=>ue(n.id)},...f.length?[{type:"divider"},...f.map(d=>({label:`${d.trigger?.label||d.title||d.slug}\u2026`,icon:d.trigger?.icon||"zap",disabled:L.peek(),action:()=>at(d,n)}))]:[],...s?[{type:"divider"},{label:`Filter by ${s.label||s.name}`,icon:"filter",submenu:[...m.map(d=>({label:d.label,action:()=>Se(s,d.op,d.value)})),{label:`${s.label||s.name} contains\u2026`,action:async()=>{const d=await E.prompt(`Show entries whose ${U(s.label||s.name)} contains`,{inputPlaceholder:"text"});d&&String(d).trim()&&Se(s,"contains",String(d).trim())}}]}]:[],...w.peek().length?[{label:`Clear filters (${w.peek().length})`,icon:"x",action:()=>{w.value=[]}}]:[],{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>xe(n)}]}const q=y.querySelector(".cex-rows"),W=y.querySelector(".cex-head"),fe=y.querySelector(".cex-chips-list");fe.innerHTML=Lt;const ae=mt(y.querySelector(".dm-loading-slot"),ft.table);try{u.value=await k.collections.get(a)}catch(e){H.value=`Could not load the collection: ${e.message||e}`}if(!c()){ae.done();return}if(!u.peek()&&(await ae.done(),!H.peek())){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const Ae=de.peek();q.innerHTML=qt(Ae.length),W.replaceChildren();const se=(e,t)=>{const n=document.createElement("th");return e&&(n.className=e),t&&n.append(t),W.appendChild(n),n},A=document.createElement("input");A.type="checkbox",A.title="Select every entry shown",A.setAttribute("aria-label","Select every entry shown"),se("cex-check",A);const Me=(e,t)=>{const n=document.createElement("button");return n.type="button",n.dataset.sort=e,n.append(document.createTextNode(t),Object.assign(document.createElement("span"),{className:"cex-sort"})),n};Ae.forEach(e=>se("",Me(e.name,e.label||e.name))),se("cex-when",Me("created","Created")),se("cex-tools"),l(M.effect(()=>{const e=Z.value;W.querySelectorAll("[data-sort]").forEach(t=>{t.querySelector(".cex-sort").textContent=t.dataset.sort===e.key?e.dir==="asc"?"\u25B2":"\u25BC":"",t.parentElement.setAttribute("aria-sort",t.dataset.sort===e.key?e.dir==="asc"?"ascending":"descending":"none")})})),l(M.effect(()=>{const e=D.value.map(n=>n.id),t=new Set(g.value);A.checked=e.length>0&&e.every(n=>t.has(n)),A.indeterminate=!A.checked&&e.some(n=>t.has(n))}));const Le=e=>{const t=e.target.closest("[data-sort]");if(!t)return;const n=Z.peek();Z.value={key:t.dataset.sort,dir:n.key===t.dataset.sort&&n.dir==="asc"?"desc":"asc"}};W.addEventListener("click",Le),A.addEventListener("change",()=>{const e=D.peek().map(t=>t.id);g.value=A.checked?[...new Set([...g.peek(),...e])]:g.peek().filter(t=>!e.includes(t))}),l(()=>W.removeEventListener("click",Le));const it=M.applyBindings(Xe,y,{methods:{clearSelection:()=>{g.value=[]},deleteSelected:()=>et(),clearAll:()=>tt(),clearFilters:()=>{w.value=[]},showMore:()=>{D.peek().length>N.peek()?N.value=N.peek()+X:Ye()}}});l(()=>it.dispose()),l(ze(y,{position:"bottom"})),l(M.effect(()=>{re.value,w.value,N.value=X}));const qe=e=>{const t=e.target.closest("button[data-idx]");if(!t)return;const n=Number(t.dataset.idx);w.value=w.peek().filter((o,s)=>s!==n)};fe.addEventListener("click",qe),l(()=>fe.removeEventListener("click",qe));const rt=E.contextMenu(q,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:(e,t)=>lt(e,t)});l(()=>rt?.destroy?.());const Te=e=>{const t=e.target.closest(".cex-row");if(!t)return;if(e.target.closest(".cex-kebab, .cex-run")){je(e.target.closest(".cex-kebab, .cex-run"));return}if(e.target.closest(".cex-check"))return;const n=J(t.dataset.id);n&&z(n)},Ne=e=>{if(!e.target.matches(".cex-sel"))return;const t=e.target.closest(".cex-row");t&&ue(t.dataset.id)},Ie=e=>{const t=e.target.closest(".cex-row");if(!t||e.target!==t)return;const n=J(t.dataset.id);n&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),je(t)):e.key==="Enter"?(e.preventDefault(),z(n)):e.key===" "?(e.preventDefault(),ue(n.id)):e.key==="Delete"&&(e.preventDefault(),xe(n)))};q.addEventListener("click",Te),q.addEventListener("change",Ne),q.addEventListener("keydown",Ie),l(()=>{q.removeEventListener("click",Te),q.removeEventListener("change",Ne),q.removeEventListener("keydown",Ie)});const De=u.peek(),oe=p.get(0).querySelector(".dm-plugin-banner-name")?.querySelector(".dm-plugin-banner-sep");if(oe&&De?.title){for(;oe.nextSibling;)oe.nextSibling.remove();oe.after(document.createTextNode(De.title))}j(p,{id:"entries-back",icon:"arrow-left",title:"All collections",onClick:()=>{location.hash="#/collections"}}),j(p,{id:"entries-fields",icon:"edit-3",title:"Edit its fields and settings",onClick:()=>{location.hash=`#/collections/edit/${encodeURIComponent(a)}`}}),Ct(a)&&j(p,{id:"entries-form",icon:"layout",title:"Open its form",onClick:()=>{location.hash=`#/forms/edit/${encodeURIComponent(a)}`}}),j(p,{id:"entries-export",icon:"download",title:"Export",onClick:()=>ct()}),j(p,{id:"entries-import",icon:"upload",title:"Import",onClick:()=>ot()}),j(p,{id:"entries-add",icon:"plus",title:"Add entry",onClick:()=>z(null)});const Oe=e=>{O?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Oe),l(()=>window.removeEventListener("beforeunload",Oe)),l(()=>{if(O?.dirty())try{S.set(B(a),{id:O.id,values:O.values(),at:new Date().toISOString()})}catch{}Ce=!0,[...pe].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(y),!u.peek()){Y.value=!1;return}if(await Ze(),!c()){ae.done();return}if(await K({pending:ae}),!c())return;let C=null;try{C=S.get(B(a))}catch{}if(C?.values){try{S.remove?S.remove(B(a)):S.set(B(a),null)}catch{}let e=C.id?J(C.id):null;if(C.id&&!e&&(e=await k.collections.getEntry(a,C.id).catch(()=>null)),!c()||C.id&&!e)return;const t=e?`your changes to \u201C${U(G(e))}\u201D`:"a new entry",n=e?.meta?.updatedAt&&C.at&&e.meta.updatedAt>C.at?" It has been changed since - check before saving.":"";await E.confirm(`You left ${t} unsaved.${n} Pick up where you left off?`)&&c()&&z(e,C.values)}}};
|