domma-cms 0.42.0 → 0.44.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/CLAUDE.md +36 -2
- package/admin/js/templates/collection-editor.html +1 -1
- package/admin/js/templates/effects.html +752 -752
- package/admin/js/templates/forms.html +17 -17
- package/admin/js/templates/my-profile.html +17 -17
- package/admin/js/templates/role-editor.html +70 -70
- package/admin/js/templates/roles.html +10 -10
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/block-editor.js +2 -2
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/form-editor.js +6 -6
- package/admin/js/views/navigation.js +16 -16
- package/admin/js/views/page-editor.js +41 -41
- package/admin/js/views/view-editor.js +1 -1
- package/bin/lib/config-merge.js +44 -44
- package/config/plugins.json +1 -1
- package/config/site.json +86 -86
- package/package.json +1 -1
- package/public/css/site.css +1 -1
- package/public/js/collection-context.js +2 -2
- package/public/js/collection-export.mjs +3 -0
- package/public/js/collection-query.mjs +1 -0
- package/public/js/collection-sort.mjs +1 -0
- package/public/js/site.js +1 -1
- package/scripts/build.js +94 -0
- package/scripts/setup.js +23 -2
- package/server/middleware/auth.js +253 -253
- package/server/routes/api/auth.js +309 -309
- package/server/routes/api/navigation.js +42 -42
- package/server/services/collections.js +12 -11
- package/server/services/email.js +167 -167
- package/server/services/markdown.js +106 -33
- package/server/services/userProfiles.js +199 -199
- package/server/services/users.js +302 -302
- package/server/templates/page.html +1 -1
- package/config/navigation.json.bak +0 -31
|
@@ -14,6 +14,7 @@ import {getCollection, listEntries} from './collections.js';
|
|
|
14
14
|
import {getMenu, resolveLocation, resolveMenuDecorations} from './menus.js';
|
|
15
15
|
import {buildMenuNav} from './menuRender.js';
|
|
16
16
|
import {checkVisibility} from '../middleware/auth.js';
|
|
17
|
+
import {getRoleHierarchy} from './roles.js';
|
|
17
18
|
|
|
18
19
|
const __dirname_md = path.dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const BLOCKS_DIR = path.resolve(__dirname_md, '../../content/blocks');
|
|
@@ -174,6 +175,51 @@ function formatDate(input, fmt) {
|
|
|
174
175
|
t => String(tokens[t]()));
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
const EXPORT_FORMATS = ['csv', 'json'];
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Decide whether a display offers export, and to whom. First match wins:
|
|
182
|
+
* 1. schema export.enabled === false → never (exportable="true" ignored)
|
|
183
|
+
* 2. exportable="true" on the shortcode → public (grandfathered: that is
|
|
184
|
+
* exactly what the attribute has always meant)
|
|
185
|
+
* 3. schema export.access → as configured
|
|
186
|
+
* 4. otherwise → the least-privileged role
|
|
187
|
+
*
|
|
188
|
+
* @param {object|undefined} schemaExport - schema.json `export` block
|
|
189
|
+
* @param {object} attrs - shortcode attributes
|
|
190
|
+
* @param {string} defaultRole - least-privileged role name
|
|
191
|
+
*/
|
|
192
|
+
export function resolveExportConfig(schemaExport, attrs, defaultRole) {
|
|
193
|
+
const cfg = schemaExport || {};
|
|
194
|
+
if (cfg.enabled === false) return {allowed: false, access: null, formats: [], roles: []};
|
|
195
|
+
const formats = (Array.isArray(cfg.formats) ? cfg.formats : EXPORT_FORMATS)
|
|
196
|
+
.filter(f => EXPORT_FORMATS.includes(f));
|
|
197
|
+
const access = attrs?.exportable === 'true' ? 'public' : (cfg.access || defaultRole);
|
|
198
|
+
return {allowed: true, access, formats, roles: rolesAtOrAbove(access)};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The role names permitted by an `access` setting — the named role plus every
|
|
203
|
+
* more-privileged one above it.
|
|
204
|
+
*
|
|
205
|
+
* The browser cannot do this itself: it learns the visitor's role name from
|
|
206
|
+
* /api/auth/me but has no copy of the hierarchy, so without this list it could
|
|
207
|
+
* only ask "is anyone signed in?" and every role setting would behave
|
|
208
|
+
* identically. Resolving it server-side keeps the check a single membership
|
|
209
|
+
* test on the client.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} access - 'public', or a role name
|
|
212
|
+
* @returns {string[]} empty for 'public' (no check needed) or an unknown role
|
|
213
|
+
*/
|
|
214
|
+
function rolesAtOrAbove(access) {
|
|
215
|
+
if (access === 'public') return [];
|
|
216
|
+
const hierarchy = getRoleHierarchy(); // most → least privileged
|
|
217
|
+
const idx = hierarchy.indexOf(access);
|
|
218
|
+
// Unknown role name: fail closed with an empty list rather than granting
|
|
219
|
+
// everyone, so a typo in schema.json cannot open export up.
|
|
220
|
+
return idx === -1 ? [] : hierarchy.slice(0, idx + 1);
|
|
221
|
+
}
|
|
222
|
+
|
|
177
223
|
/**
|
|
178
224
|
* Build the `data-ctx` attribute that powers the public-site right-click
|
|
179
225
|
* search/filter menu (public/js/collection-context.js). The payload carries
|
|
@@ -182,9 +228,12 @@ function formatDate(input, fmt) {
|
|
|
182
228
|
*
|
|
183
229
|
* @param {Array<{name: string, label?: string}>} fields - displayed fields
|
|
184
230
|
* @param {object[]} entries - collection entries {id, data}
|
|
231
|
+
* @param {object} [opts]
|
|
232
|
+
* @param {string} [opts.slug] - collection slug (for export requests)
|
|
233
|
+
* @param {object} [opts.export] - resolveExportConfig() result
|
|
185
234
|
* @returns {string} ` data-ctx="<base64 JSON>"` (leading space included)
|
|
186
235
|
*/
|
|
187
|
-
function buildCtxAttr(fields, entries) {
|
|
236
|
+
function buildCtxAttr(fields, entries, opts = {}) {
|
|
188
237
|
const names = [...new Set(fields.map(f => f?.name).filter(Boolean))];
|
|
189
238
|
if (!names.length || !entries.length) return '';
|
|
190
239
|
const payload = {
|
|
@@ -194,6 +243,8 @@ function buildCtxAttr(fields, entries) {
|
|
|
194
243
|
data: Object.fromEntries(names.map(n => [n, e.data?.[n] ?? '']))
|
|
195
244
|
}))
|
|
196
245
|
};
|
|
246
|
+
if (opts.slug) payload.slug = opts.slug;
|
|
247
|
+
if (opts.export) payload.export = opts.export;
|
|
197
248
|
return ` data-ctx="${Buffer.from(JSON.stringify(payload), 'utf8').toString('base64')}"`;
|
|
198
249
|
}
|
|
199
250
|
|
|
@@ -255,7 +306,7 @@ function interpolateBlockTemplate(blockTemplate, e) {
|
|
|
255
306
|
* @param {object|null} ctaOpts - Optional CTA button options
|
|
256
307
|
* @returns {string}
|
|
257
308
|
*/
|
|
258
|
-
function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols, blockName = '', blockCss = '') {
|
|
309
|
+
function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols, blockName = '', blockCss = '', ctxOpts = {}) {
|
|
259
310
|
if (!entries.length) {
|
|
260
311
|
return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
|
|
261
312
|
}
|
|
@@ -287,7 +338,7 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols,
|
|
|
287
338
|
? `dm-collection-display dm-collection-blocks grid ${responsiveGridCols(validCols).join(' ')} gap-4`
|
|
288
339
|
: 'dm-collection-display dm-collection-blocks';
|
|
289
340
|
|
|
290
|
-
const ctx = buildCtxAttr(blockTemplateFields(blockTemplate), entries);
|
|
341
|
+
const ctx = buildCtxAttr(blockTemplateFields(blockTemplate), entries, ctxOpts);
|
|
291
342
|
const styleTag = buildBlockStyleTag(blockName, blockCss);
|
|
292
343
|
return `${styleTag}<div class="${wrapperClass}"${ctx}>\n${items.join('\n')}\n</div>`;
|
|
293
344
|
}
|
|
@@ -456,7 +507,7 @@ function displayValueHtml(entry, field) {
|
|
|
456
507
|
return escapeHtmlText(displayValue(entry, field));
|
|
457
508
|
}
|
|
458
509
|
|
|
459
|
-
function renderCollectionTable(slug, entries, visibleFields, attrs, ctaOpts) {
|
|
510
|
+
function renderCollectionTable(slug, entries, visibleFields, attrs, ctaOpts, ctxOpts = {}) {
|
|
460
511
|
const columns = visibleFields.map(f => ({key: f.name, title: f.label || f.name}));
|
|
461
512
|
const rows = entries.map(e => {
|
|
462
513
|
const row = {};
|
|
@@ -474,14 +525,16 @@ function renderCollectionTable(slug, entries, visibleFields, attrs, ctaOpts) {
|
|
|
474
525
|
sortable: attrs.sortable !== 'false',
|
|
475
526
|
exportable: attrs.exportable === 'true',
|
|
476
527
|
pageSize: parseInt(attrs['page-size'], 10) || 25,
|
|
477
|
-
empty: attrs.empty || 'No entries found'
|
|
528
|
+
empty: attrs.empty || 'No entries found',
|
|
529
|
+
slug: ctxOpts.slug || slug,
|
|
530
|
+
exportCfg: ctxOpts.export || {allowed: false, access: null, formats: []}
|
|
478
531
|
};
|
|
479
532
|
if (ctaOpts) tableOpts.ctaConfig = ctaOpts;
|
|
480
533
|
const payload = Buffer.from(JSON.stringify(tableOpts)).toString('base64');
|
|
481
534
|
return `<div class="dm-collection-display" data-collection-table data-slug="${escapeAttr(slug)}" data-payload="${payload}"></div>`;
|
|
482
535
|
}
|
|
483
536
|
|
|
484
|
-
function renderCollectionCards(entries, visibleFields, titleField, columns, emptyMsg, ctaOpts) {
|
|
537
|
+
function renderCollectionCards(entries, visibleFields, titleField, columns, emptyMsg, ctaOpts, ctxOpts = {}) {
|
|
485
538
|
const cols = ['2', '3', '4'].includes(String(columns)) ? columns : '3';
|
|
486
539
|
if (!entries.length) {
|
|
487
540
|
return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
|
|
@@ -506,10 +559,10 @@ function renderCollectionCards(entries, visibleFields, titleField, columns, empt
|
|
|
506
559
|
}
|
|
507
560
|
return `<div class="card" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${title ? `<div class="card-header">${title}</div>` : ''}<div class="card-body">${body || ' '}</div>${footer}</div>`;
|
|
508
561
|
}).join('\n');
|
|
509
|
-
return `<div class="dm-collection-display grid ${responsiveGridCols(cols).join(' ')} gap-4"${buildCtxAttr(visibleFields, entries)}>\n${cards}\n</div>`;
|
|
562
|
+
return `<div class="dm-collection-display grid ${responsiveGridCols(cols).join(' ')} gap-4"${buildCtxAttr(visibleFields, entries, ctxOpts)}>\n${cards}\n</div>`;
|
|
510
563
|
}
|
|
511
564
|
|
|
512
|
-
function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaOpts) {
|
|
565
|
+
function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaOpts, ctxOpts = {}) {
|
|
513
566
|
if (!entries.length) {
|
|
514
567
|
return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
|
|
515
568
|
}
|
|
@@ -533,7 +586,7 @@ function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaO
|
|
|
533
586
|
}
|
|
534
587
|
return `<div class="dm-collection-list-item" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${title ? `<strong>${title}</strong>` : ''}${rest}${ctaHtml}</div>`;
|
|
535
588
|
}).join('\n');
|
|
536
|
-
return `<div class="dm-collection-display dm-collection-list"${buildCtxAttr(visibleFields, entries)}>\n${items}\n</div>`;
|
|
589
|
+
return `<div class="dm-collection-display dm-collection-list"${buildCtxAttr(visibleFields, entries, ctxOpts)}>\n${items}\n</div>`;
|
|
537
590
|
}
|
|
538
591
|
|
|
539
592
|
/**
|
|
@@ -545,7 +598,7 @@ function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaO
|
|
|
545
598
|
* @param {string} emptyMsg
|
|
546
599
|
* @returns {string}
|
|
547
600
|
*/
|
|
548
|
-
function renderCollectionAccordion(entries, titleField, bodyField, multiple, emptyMsg) {
|
|
601
|
+
function renderCollectionAccordion(entries, titleField, bodyField, multiple, emptyMsg, ctxOpts = {}) {
|
|
549
602
|
if (!entries.length) {
|
|
550
603
|
return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
|
|
551
604
|
}
|
|
@@ -564,7 +617,7 @@ function renderCollectionAccordion(entries, titleField, bodyField, multiple, emp
|
|
|
564
617
|
);
|
|
565
618
|
}).join('\n');
|
|
566
619
|
|
|
567
|
-
const ctx = buildCtxAttr([{name: titleField}, {name: bodyField}], entries);
|
|
620
|
+
const ctx = buildCtxAttr([{name: titleField}, {name: bodyField}], entries, ctxOpts);
|
|
568
621
|
return `<div class="dm-collection-display accordion"${multiAttr}${ctx}>\n${items}\n</div>`;
|
|
569
622
|
}
|
|
570
623
|
|
|
@@ -585,7 +638,7 @@ function renderCollectionAccordion(entries, titleField, bodyField, multiple, emp
|
|
|
585
638
|
* @param {string} opts.emptyMsg
|
|
586
639
|
* @returns {string}
|
|
587
640
|
*/
|
|
588
|
-
function renderCollectionTimeline(entries, opts) {
|
|
641
|
+
function renderCollectionTimeline(entries, opts, ctxOpts = {}) {
|
|
589
642
|
const {titleField, dateField, statusField, iconField, bodyField, layout, theme, mode, emptyMsg} = opts;
|
|
590
643
|
if (!entries.length) {
|
|
591
644
|
return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
|
|
@@ -604,7 +657,8 @@ function renderCollectionTimeline(entries, opts) {
|
|
|
604
657
|
|
|
605
658
|
const ctx = buildCtxAttr(
|
|
606
659
|
[{name: titleField}, {name: dateField}, {name: statusField}, {name: bodyField}].filter(f => f.name),
|
|
607
|
-
entries
|
|
660
|
+
entries,
|
|
661
|
+
ctxOpts
|
|
608
662
|
);
|
|
609
663
|
return `<div class="dm-collection-display dm-progression" data-layout="${layout}" data-theme="${theme}" data-mode="${mode}"${ctx}>\n${items}\n</div>`;
|
|
610
664
|
}
|
|
@@ -630,7 +684,7 @@ function renderCollectionTimeline(entries, opts) {
|
|
|
630
684
|
* @param {string} opts.emptyMsg
|
|
631
685
|
* @returns {string}
|
|
632
686
|
*/
|
|
633
|
-
function renderCollectionCarousel(entries, opts) {
|
|
687
|
+
function renderCollectionCarousel(entries, opts, ctxOpts = {}) {
|
|
634
688
|
const {imageField, titleField, bodyField, blockTemplate, blockName, blockCss, dataAttrs = {}, emptyMsg} = opts;
|
|
635
689
|
if (!entries.length) {
|
|
636
690
|
return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
|
|
@@ -664,7 +718,7 @@ function renderCollectionCarousel(entries, opts) {
|
|
|
664
718
|
const ctxFields = blockTemplate
|
|
665
719
|
? blockTemplateFields(blockTemplate)
|
|
666
720
|
: [{name: titleField}, {name: bodyField}].filter(f => f.name);
|
|
667
|
-
const ctx = buildCtxAttr(ctxFields, entries);
|
|
721
|
+
const ctx = buildCtxAttr(ctxFields, entries, ctxOpts);
|
|
668
722
|
const styleTag = blockTemplate ? buildBlockStyleTag(blockName, blockCss) : '';
|
|
669
723
|
return `${styleTag}<div class="dm-collection-display carousel"${passthrough}${ctx}><div class="carousel-track">${slides}</div></div>`;
|
|
670
724
|
}
|
|
@@ -717,7 +771,7 @@ async function loadOptionalBlockAssets(blockName) {
|
|
|
717
771
|
}
|
|
718
772
|
}
|
|
719
773
|
|
|
720
|
-
function renderCollectionListgroup(entries, opts) {
|
|
774
|
+
function renderCollectionListgroup(entries, opts, ctxOpts = {}) {
|
|
721
775
|
const {titleField, bodyField, variant, size, itemVariant, action, blockTemplate, blockName, blockCss, emptyMsg} = opts;
|
|
722
776
|
if (!entries.length) {
|
|
723
777
|
return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
|
|
@@ -757,7 +811,7 @@ function renderCollectionListgroup(entries, opts) {
|
|
|
757
811
|
const ctxFields = blockTemplate
|
|
758
812
|
? blockTemplateFields(blockTemplate)
|
|
759
813
|
: [{name: titleField}, {name: bodyField}].filter(f => f.name);
|
|
760
|
-
const ctx = buildCtxAttr(ctxFields, entries);
|
|
814
|
+
const ctx = buildCtxAttr(ctxFields, entries, ctxOpts);
|
|
761
815
|
const styleTag = blockTemplate ? buildBlockStyleTag(blockName, blockCss) : '';
|
|
762
816
|
return `${styleTag}<div class="${wrapperClasses.join(' ')}"${ctx}>\n${items}\n</div>`;
|
|
763
817
|
}
|
|
@@ -826,6 +880,19 @@ async function processViewBlocks(markdown, tagSet) {
|
|
|
826
880
|
// Map MongoDB docs to { id, data } shape. MongoAdapter docs have a top-level id field.
|
|
827
881
|
const entries = results.map(doc => ({id: doc.id || '', data: doc.data || doc}));
|
|
828
882
|
|
|
883
|
+
// Resolve the view's underlying collection schema to compute export
|
|
884
|
+
// permission. Views don't own a schema themselves — [dis]allowing
|
|
885
|
+
// export must fall back CLOSED if the source collection can't be
|
|
886
|
+
// resolved, since an open fallback would be a permission bypass.
|
|
887
|
+
const sourceSlug = viewConfig?.pipeline?.source || '';
|
|
888
|
+
const sourceSchema = sourceSlug ? await getCollection(sourceSlug) : null;
|
|
889
|
+
const ctxOpts = sourceSchema
|
|
890
|
+
? {
|
|
891
|
+
slug: sourceSlug,
|
|
892
|
+
export: resolveExportConfig(sourceSchema.export, attrs, getRoleHierarchy().at(-1))
|
|
893
|
+
}
|
|
894
|
+
: {slug: sourceSlug || undefined, export: {allowed: false, access: null, formats: []}};
|
|
895
|
+
|
|
829
896
|
// Derive visible fields from attr filter or first entry's data keys
|
|
830
897
|
let fields;
|
|
831
898
|
if (fieldFilter?.length) {
|
|
@@ -841,14 +908,14 @@ async function processViewBlocks(markdown, tagSet) {
|
|
|
841
908
|
const display = displayAttr || viewConfig?.display?.mode || 'table';
|
|
842
909
|
|
|
843
910
|
if (display === 'cards') {
|
|
844
|
-
replacement = renderCollectionCards(entries, fields, titleField, columns, emptyMsg, ctaOpts);
|
|
911
|
+
replacement = renderCollectionCards(entries, fields, titleField, columns, emptyMsg, ctaOpts, ctxOpts);
|
|
845
912
|
} else if (display === 'list') {
|
|
846
|
-
replacement = renderCollectionList(entries, fields, titleField, emptyMsg, ctaOpts);
|
|
913
|
+
replacement = renderCollectionList(entries, fields, titleField, emptyMsg, ctaOpts, ctxOpts);
|
|
847
914
|
} else if (display === 'accordion') {
|
|
848
915
|
const accordionTitleField = attrs['title-field'] || 'title';
|
|
849
916
|
const bodyField = attrs['body-field'] || 'description';
|
|
850
917
|
const multiple = attrs.multiple === 'true';
|
|
851
|
-
replacement = renderCollectionAccordion(entries, accordionTitleField, bodyField, multiple, emptyMsg);
|
|
918
|
+
replacement = renderCollectionAccordion(entries, accordionTitleField, bodyField, multiple, emptyMsg, ctxOpts);
|
|
852
919
|
} else if (display === 'timeline') {
|
|
853
920
|
const timelineLayout = ['vertical', 'centred', 'horizontal'].includes(attrs.layout) ? attrs.layout : 'vertical';
|
|
854
921
|
const timelineTheme = ['minimal', 'corporate', 'modern'].includes(attrs.theme) ? attrs.theme : 'minimal';
|
|
@@ -863,7 +930,7 @@ async function processViewBlocks(markdown, tagSet) {
|
|
|
863
930
|
theme: timelineTheme,
|
|
864
931
|
mode: timelineMode,
|
|
865
932
|
emptyMsg,
|
|
866
|
-
});
|
|
933
|
+
}, ctxOpts);
|
|
867
934
|
} else if (display === 'carousel') {
|
|
868
935
|
const {tpl, css, missing} = await loadOptionalBlockAssets(attrs.block);
|
|
869
936
|
replacement = missing
|
|
@@ -877,7 +944,7 @@ async function processViewBlocks(markdown, tagSet) {
|
|
|
877
944
|
blockCss: css,
|
|
878
945
|
dataAttrs: attrs,
|
|
879
946
|
emptyMsg,
|
|
880
|
-
});
|
|
947
|
+
}, ctxOpts);
|
|
881
948
|
} else if (display === 'listgroup') {
|
|
882
949
|
const {tpl, css, missing} = await loadOptionalBlockAssets(attrs.block);
|
|
883
950
|
replacement = missing
|
|
@@ -893,7 +960,7 @@ async function processViewBlocks(markdown, tagSet) {
|
|
|
893
960
|
blockName: attrs.block || '',
|
|
894
961
|
blockCss: css,
|
|
895
962
|
emptyMsg,
|
|
896
|
-
});
|
|
963
|
+
}, ctxOpts);
|
|
897
964
|
} else if (display === 'block') {
|
|
898
965
|
const blockName = attrs.block || viewConfig?.display?.block || '';
|
|
899
966
|
if (blockName) {
|
|
@@ -902,13 +969,13 @@ async function processViewBlocks(markdown, tagSet) {
|
|
|
902
969
|
loadBlockTemplate(blockName),
|
|
903
970
|
loadBlockCss(blockName),
|
|
904
971
|
]);
|
|
905
|
-
replacement = renderCollectionBlocks(entries, tpl, emptyMsg, ctaOpts, '', blockName, css);
|
|
972
|
+
replacement = renderCollectionBlocks(entries, tpl, emptyMsg, ctaOpts, '', blockName, css, ctxOpts);
|
|
906
973
|
} catch {
|
|
907
974
|
replacement = blockNotFoundHtml(blockName);
|
|
908
975
|
}
|
|
909
976
|
}
|
|
910
977
|
} else {
|
|
911
|
-
replacement = renderCollectionTable(`view:${slug}`, entries, fields, attrs, ctaOpts);
|
|
978
|
+
replacement = renderCollectionTable(`view:${slug}`, entries, fields, attrs, ctaOpts, ctxOpts);
|
|
912
979
|
}
|
|
913
980
|
} catch {
|
|
914
981
|
// View not found or pipeline error — show empty state
|
|
@@ -1306,6 +1373,12 @@ export async function renderCollectionFragment(attrs, { extraFilter = null } = {
|
|
|
1306
1373
|
const schema = await getCollection(slug);
|
|
1307
1374
|
if (!schema) throw new Error('not found');
|
|
1308
1375
|
|
|
1376
|
+
const defaultRole = getRoleHierarchy().at(-1);
|
|
1377
|
+
const ctxOpts = {
|
|
1378
|
+
slug: schema.slug,
|
|
1379
|
+
export: resolveExportConfig(schema.export, attrs, defaultRole)
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1309
1382
|
const filter = buildShortcodeFilter(attrs);
|
|
1310
1383
|
if (extraFilter) Object.assign(filter, extraFilter);
|
|
1311
1384
|
|
|
@@ -1328,14 +1401,14 @@ export async function renderCollectionFragment(attrs, { extraFilter = null } = {
|
|
|
1328
1401
|
}
|
|
1329
1402
|
|
|
1330
1403
|
if (display === 'cards') {
|
|
1331
|
-
replacement = renderCollectionCards(entries, fields, titleField, columns, emptyMsg, ctaOpts);
|
|
1404
|
+
replacement = renderCollectionCards(entries, fields, titleField, columns, emptyMsg, ctaOpts, ctxOpts);
|
|
1332
1405
|
} else if (display === 'list') {
|
|
1333
|
-
replacement = renderCollectionList(entries, fields, titleField, emptyMsg, ctaOpts);
|
|
1406
|
+
replacement = renderCollectionList(entries, fields, titleField, emptyMsg, ctaOpts, ctxOpts);
|
|
1334
1407
|
} else if (display === 'accordion') {
|
|
1335
1408
|
const accordionTitleField = attrs['title-field'] || 'title';
|
|
1336
1409
|
const bodyField = attrs['body-field'] || 'description';
|
|
1337
1410
|
const multiple = attrs.multiple === 'true';
|
|
1338
|
-
replacement = renderCollectionAccordion(entries, accordionTitleField, bodyField, multiple, emptyMsg);
|
|
1411
|
+
replacement = renderCollectionAccordion(entries, accordionTitleField, bodyField, multiple, emptyMsg, ctxOpts);
|
|
1339
1412
|
} else if (display === 'timeline') {
|
|
1340
1413
|
const timelineLayout = ['vertical', 'centred', 'horizontal'].includes(attrs.layout) ? attrs.layout : 'vertical';
|
|
1341
1414
|
const timelineTheme = ['minimal', 'corporate', 'modern'].includes(attrs.theme) ? attrs.theme : 'minimal';
|
|
@@ -1350,7 +1423,7 @@ export async function renderCollectionFragment(attrs, { extraFilter = null } = {
|
|
|
1350
1423
|
theme: timelineTheme,
|
|
1351
1424
|
mode: timelineMode,
|
|
1352
1425
|
emptyMsg,
|
|
1353
|
-
});
|
|
1426
|
+
}, ctxOpts);
|
|
1354
1427
|
} else if (display === 'carousel') {
|
|
1355
1428
|
const {tpl, css, missing} = await loadOptionalBlockAssets(attrs.block);
|
|
1356
1429
|
replacement = missing
|
|
@@ -1364,7 +1437,7 @@ export async function renderCollectionFragment(attrs, { extraFilter = null } = {
|
|
|
1364
1437
|
blockCss: css,
|
|
1365
1438
|
dataAttrs: attrs,
|
|
1366
1439
|
emptyMsg,
|
|
1367
|
-
});
|
|
1440
|
+
}, ctxOpts);
|
|
1368
1441
|
} else if (display === 'listgroup') {
|
|
1369
1442
|
const {tpl, css, missing} = await loadOptionalBlockAssets(attrs.block);
|
|
1370
1443
|
replacement = missing
|
|
@@ -1380,7 +1453,7 @@ export async function renderCollectionFragment(attrs, { extraFilter = null } = {
|
|
|
1380
1453
|
blockName: attrs.block || '',
|
|
1381
1454
|
blockCss: css,
|
|
1382
1455
|
emptyMsg,
|
|
1383
|
-
});
|
|
1456
|
+
}, ctxOpts);
|
|
1384
1457
|
} else if (display === 'block') {
|
|
1385
1458
|
const blockName = attrs.block || '';
|
|
1386
1459
|
if (blockName) {
|
|
@@ -1390,13 +1463,13 @@ export async function renderCollectionFragment(attrs, { extraFilter = null } = {
|
|
|
1390
1463
|
loadBlockCss(blockName),
|
|
1391
1464
|
]);
|
|
1392
1465
|
const cols = attrs.cols || '';
|
|
1393
|
-
replacement = renderCollectionBlocks(entries, tpl, emptyMsg, ctaOpts, cols, blockName, css);
|
|
1466
|
+
replacement = renderCollectionBlocks(entries, tpl, emptyMsg, ctaOpts, cols, blockName, css, ctxOpts);
|
|
1394
1467
|
} catch {
|
|
1395
1468
|
replacement = blockNotFoundHtml(blockName);
|
|
1396
1469
|
}
|
|
1397
1470
|
}
|
|
1398
1471
|
} else {
|
|
1399
|
-
replacement = renderCollectionTable(slug, entries, fields, attrs, ctaOpts);
|
|
1472
|
+
replacement = renderCollectionTable(slug, entries, fields, attrs, ctaOpts, ctxOpts);
|
|
1400
1473
|
}
|
|
1401
1474
|
} catch {
|
|
1402
1475
|
// Collection not found or read error — show empty message
|