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/server/services/forms.js
CHANGED
|
@@ -9,6 +9,38 @@ import {fileURLToPath} from 'url';
|
|
|
9
9
|
import * as cache from './cache/index.js';
|
|
10
10
|
import {buttonSettingsProblems, formClassProblems} from '../../public/js/form-submit.mjs';
|
|
11
11
|
import {fileIn} from './safePath.js';
|
|
12
|
+
import {getConfig} from '../config.js';
|
|
13
|
+
|
|
14
|
+
/** What one submission's files may hold in memory together when server.json sets nothing: 50 MB. */
|
|
15
|
+
export const DEFAULT_MAX_SUBMISSION_BYTES = 50 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The most bytes one submission's files may hold in memory, all fields together:
|
|
19
|
+
* server.json `uploads.maxSubmissionBytes` (read fresh) when set; otherwise 50 MB or
|
|
20
|
+
* `uploads.maxFileSize`, whichever is larger, so one file the site takes is never
|
|
21
|
+
* refused for the total.
|
|
22
|
+
*
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function maxSubmissionBytes() {
|
|
26
|
+
let uploads = {};
|
|
27
|
+
try { uploads = getConfig('server')?.uploads || {}; } catch { /* no server.json */ }
|
|
28
|
+
const set = Number(uploads.maxSubmissionBytes);
|
|
29
|
+
if (set > 0) return set;
|
|
30
|
+
return Math.max(DEFAULT_MAX_SUBMISSION_BYTES, Number(uploads.maxFileSize) || 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A form as a page draws it, told the per-submission file cap when it has a file field,
|
|
35
|
+
* so the page refuses a file that would pass it before anything is sent.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} form
|
|
38
|
+
* @returns {object}
|
|
39
|
+
*/
|
|
40
|
+
export function publicFileLimits(form) {
|
|
41
|
+
const hasFiles = (form?.fields || []).some(f => f && f.type === 'file');
|
|
42
|
+
return hasFiles ? {...form, fileLimits: {maxTotalBytes: maxSubmissionBytes()}} : form;
|
|
43
|
+
}
|
|
12
44
|
|
|
13
45
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
46
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -44,6 +76,11 @@ export async function readForm(slug) {
|
|
|
44
76
|
*/
|
|
45
77
|
export async function writeForm(slug, data) {
|
|
46
78
|
await ensureFormsDir();
|
|
79
|
+
// fileLimits is the server's config, added to the copy a page is given - never stored
|
|
80
|
+
if (data && 'fileLimits' in data) {
|
|
81
|
+
const {fileLimits: _limits, ...rest} = data;
|
|
82
|
+
data = rest;
|
|
83
|
+
}
|
|
47
84
|
const file = fileIn(FORMS_DIR, slug, '.json');
|
|
48
85
|
await fs.writeFile(file, JSON.stringify(data, null, 4) + '\n', 'utf8');
|
|
49
86
|
await cache.invalidateTags([`form:${slug}`]);
|
|
@@ -198,7 +235,9 @@ function toFormFieldType(type) {
|
|
|
198
235
|
string: 'string', text: 'string', email: 'email', tel: 'tel',
|
|
199
236
|
number: 'number', textarea: 'textarea', select: 'select', radio: 'radio',
|
|
200
237
|
checkbox: 'checkbox', 'checkbox-group': 'checkbox-group',
|
|
201
|
-
date: 'date', time: 'time', url: 'url', hidden: 'hidden'
|
|
238
|
+
date: 'date', time: 'time', url: 'url', hidden: 'hidden',
|
|
239
|
+
// {url, name, size, mime} - a list of them when the field is `multiple`
|
|
240
|
+
file: 'file'
|
|
202
241
|
};
|
|
203
242
|
return map[type] || 'string';
|
|
204
243
|
}
|
|
@@ -223,6 +262,7 @@ function buildFormFromCollection(schema) {
|
|
|
223
262
|
};
|
|
224
263
|
if (f.options) field.options = f.options;
|
|
225
264
|
if (f.validation) field.validation = f.validation;
|
|
265
|
+
if (f.type === 'file' && f.multiple) field.multiple = true;
|
|
226
266
|
return field;
|
|
227
267
|
});
|
|
228
268
|
|
|
@@ -286,7 +326,9 @@ function toCollectionFieldType(type) {
|
|
|
286
326
|
string: 'string', email: 'email', tel: 'tel', number: 'number',
|
|
287
327
|
textarea: 'textarea', select: 'select', radio: 'radio',
|
|
288
328
|
checkbox: 'checkbox', 'checkbox-group': 'checkbox-group',
|
|
289
|
-
date: 'date', time: 'time', url: 'url', hidden: 'hidden'
|
|
329
|
+
date: 'date', time: 'time', url: 'url', hidden: 'hidden',
|
|
330
|
+
// {url, name, size, mime} - a list of them when the field is `multiple`
|
|
331
|
+
file: 'file'
|
|
290
332
|
};
|
|
291
333
|
return map[type] || 'string';
|
|
292
334
|
}
|
|
@@ -329,9 +371,12 @@ export async function ensureCollectionForForm(form) {
|
|
|
329
371
|
name: f.name,
|
|
330
372
|
type: toCollectionFieldType(f.type),
|
|
331
373
|
label: f.label || f.name,
|
|
332
|
-
required
|
|
374
|
+
// A file field's `required` is the form's to enforce (it sees the upload);
|
|
375
|
+
// copied here it would stop the entry editor saving an entry without one.
|
|
376
|
+
required: f.type === 'file' ? false : !!f.required
|
|
333
377
|
};
|
|
334
378
|
if (f.options) field.options = f.options;
|
|
379
|
+
if (f.type === 'file' && f.multiple) field.multiple = true;
|
|
335
380
|
if (f.validation) field.validation = f.validation;
|
|
336
381
|
return field;
|
|
337
382
|
});
|
|
@@ -23,6 +23,7 @@ import {buildTextStyle} from '../../public/js/text-style.mjs';
|
|
|
23
23
|
import {buildScribeStyle} from '../../public/js/scribe-style.mjs';
|
|
24
24
|
import {CHARACTERS, resolveCharacterSize} from '../../public/js/celebrations/character-catalog.mjs';
|
|
25
25
|
import {publicButtonSettings} from '../../public/js/form-submit.mjs';
|
|
26
|
+
import {publicFileLimits} from './forms.js';
|
|
26
27
|
import {applyShortcodeExpiry} from './shortcodeExpiry.js';
|
|
27
28
|
|
|
28
29
|
const __dirname_md = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -631,13 +632,29 @@ function isFileRef(v) {
|
|
|
631
632
|
return v && typeof v === 'object' && typeof v.url === 'string' && typeof v.name === 'string';
|
|
632
633
|
}
|
|
633
634
|
|
|
635
|
+
/** A `multiple` file field's value: a non-empty list of nothing but file references. */
|
|
636
|
+
function isFileRefList(v) {
|
|
637
|
+
return Array.isArray(v) && v.length > 0 && v.every(isFileRef);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** One file reference as HTML: a thumbnail link for an image, a download link otherwise. */
|
|
641
|
+
function fileRefHtml(ref) {
|
|
642
|
+
const url = escapeAttr(ref.url);
|
|
643
|
+
const name = escapeHtmlText(ref.name);
|
|
644
|
+
if (String(ref.mime || '').startsWith('image/')) {
|
|
645
|
+
return `<a href="${url}" target="_blank" rel="noopener"><img src="${url}" alt="${name}" loading="lazy" class="dm-file-thumb"></a>`;
|
|
646
|
+
}
|
|
647
|
+
return `<a href="${url}" download>${name}</a>`;
|
|
648
|
+
}
|
|
649
|
+
|
|
634
650
|
/**
|
|
635
651
|
* Resolve the display value for one field on one entry.
|
|
636
652
|
*
|
|
637
653
|
* For a `type: reference` field, returns the resolved label from `_refs`
|
|
638
654
|
* (set by `references.resolveReferences()`); arrays join with ", ", missing
|
|
639
655
|
* targets render as "<id> (missing)". For a `type: file` field (or any field
|
|
640
|
-
* holding a stored file ref), returns the filename
|
|
656
|
+
* holding a stored file ref), returns the filename - names joined with ", " for a
|
|
657
|
+
* `multiple` field's list. Everything else returns
|
|
641
658
|
* the raw value with `null`/`undefined` collapsed to '' and arrays joined.
|
|
642
659
|
*
|
|
643
660
|
* @param {object} entry
|
|
@@ -654,6 +671,7 @@ function displayValue(entry, field) {
|
|
|
654
671
|
}
|
|
655
672
|
const raw = entry.data?.[field.name];
|
|
656
673
|
if (isFileRef(raw)) return raw.name;
|
|
674
|
+
if (isFileRefList(raw)) return raw.map(f => f.name).join(', ');
|
|
657
675
|
if (raw == null) return '';
|
|
658
676
|
if (Array.isArray(raw)) return raw.join(', ');
|
|
659
677
|
return String(raw);
|
|
@@ -679,14 +697,8 @@ function displayValueHtml(entry, field) {
|
|
|
679
697
|
}
|
|
680
698
|
}
|
|
681
699
|
const raw = entry.data?.[field.name];
|
|
682
|
-
if (isFileRef(raw))
|
|
683
|
-
|
|
684
|
-
const name = escapeHtmlText(raw.name);
|
|
685
|
-
if (String(raw.mime || '').startsWith('image/')) {
|
|
686
|
-
return `<a href="${url}" target="_blank" rel="noopener"><img src="${url}" alt="${name}" loading="lazy" class="dm-file-thumb"></a>`;
|
|
687
|
-
}
|
|
688
|
-
return `<a href="${url}" download>${name}</a>`;
|
|
689
|
-
}
|
|
700
|
+
if (isFileRef(raw)) return fileRefHtml(raw);
|
|
701
|
+
if (isFileRefList(raw)) return raw.map(fileRefHtml).join(' ');
|
|
690
702
|
return escapeHtmlText(displayValue(entry, field));
|
|
691
703
|
}
|
|
692
704
|
|
|
@@ -4450,7 +4462,7 @@ async function processFormBlocks(markdown, tagSet) {
|
|
|
4450
4462
|
const {actions: _actions, ...publicForm} = form;
|
|
4451
4463
|
// A Cancel link that is not safe to draw stays out of the page source.
|
|
4452
4464
|
if (publicForm.settings) publicForm.settings = publicButtonSettings(publicForm.settings);
|
|
4453
|
-
const encoded = Buffer.from(JSON.stringify(publicForm)).toString('base64');
|
|
4465
|
+
const encoded = Buffer.from(JSON.stringify(publicFileLimits(publicForm))).toString('base64');
|
|
4454
4466
|
const extraClass = attrs.class ? ` ${escapeAttr(attrs.class)}` : '';
|
|
4455
4467
|
const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
4456
4468
|
replacement = `<div class="cms-form-embed${extraClass}" data-form-inline="${escapeAttr(encoded)}"${idAttr}>${formSummaryHtml(form)}</div>`;
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
<script src="/public/js/form-logic-engine.js?v=20261025-dropzone-disable" defer></script>
|
|
186
186
|
|
|
187
187
|
<!-- Site initialisation -->
|
|
188
|
-
<script src="/public/js/site.js?v=20261025-
|
|
188
|
+
<script src="/public/js/site.js?v=20261025-files-total" type="module"></script>
|
|
189
189
|
<script src="/public/js/collection-context.js?v=20261010-utf8-payloads" type="module"></script>
|
|
190
190
|
{{ctxMenusModule}}
|
|
191
191
|
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
<script src="/public/js/menu-float.js?v=20260927-float" type="module"></script>
|
|
196
196
|
|
|
197
197
|
<!-- Core Forms renderer -->
|
|
198
|
-
<script src="/public/js/forms.js?v=20261025-
|
|
198
|
+
<script src="/public/js/forms.js?v=20261025-forms-files-total" type="module"></script>
|
|
199
199
|
|
|
200
200
|
<!-- Core effects runtime -->
|
|
201
201
|
<script src="/public/js/effects.js?v=20261015-occasions" defer></script>
|