Release v2.2.0

This commit is contained in:
2026-08-01 21:41:44 +01:00
parent c643d2fb07
commit d6417b667c
673 changed files with 146752 additions and 7389 deletions
+144
View File
@@ -0,0 +1,144 @@
const fs = require('fs');
const path = require('path');
// General-purpose helpers used across the admin UI.
function registerGeneralHelpers(Handlebars) {
Handlebars.registerHelper('eq', function (left, right) {
return left === right;
});
Handlebars.registerHelper('gt', function (left, right) {
return left > right;
});
Handlebars.registerHelper('lt', function (left, right) {
return left < right;
});
Handlebars.registerHelper('json', function (value) {
return new Handlebars.SafeString(JSON.stringify(value).replace(/</g, '\\u003c'));
});
Handlebars.registerHelper('truncateText', function (value, maxLength) {
const text = String(value || '').trim();
const limit = Number(maxLength);
if (!text || !Number.isFinite(limit) || limit <= 0 || text.length <= limit) {
return text;
}
return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}`;
});
}
// URL helpers for generating links and external URL checks.
function registerUrlHelpers(Handlebars) {
Handlebars.registerHelper('isExternalUrl', function (value) {
return /^https?:\/\//i.test(String(value || ''));
});
Handlebars.registerHelper('assetHref', function (value) {
const href = String(value || '').trim();
if (!href) {
return '';
}
if (/^https?:\/\//i.test(href) || href.startsWith('/')) {
return href;
}
return `/assets/${href}`;
});
Handlebars.registerHelper('playerUrl', function (slug) {
const base = (process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || 'http://localhost:3001').replace(/\/$/, '');
return `${base}/screen/${encodeURIComponent(slug)}`;
});
}
// Permission helpers delegate to the RBAC layer.
function registerPermissionHelpers(Handlebars, deps) {
const hasPermission = deps && deps.hasPermission;
const hasAnyPermission = deps && deps.hasAnyPermission;
Handlebars.registerHelper('hasPermission', function (currentUser, permissionKey) {
return hasPermission(currentUser, permissionKey);
});
Handlebars.registerHelper('anyPermission', function (currentUser) {
const args = Array.prototype.slice.call(arguments, 1);
args.pop();
return hasAnyPermission(currentUser, args);
});
}
function registerUserHelpers(Handlebars) {
Handlebars.registerHelper('userInitial', function (name) {
const text = String(name || '').trim();
return text ? text.charAt(0).toUpperCase() : 'A';
});
}
// Action button helpers keep form submit controls consistent.
function registerActionHelpers(Handlebars) {
Handlebars.registerHelper('saveActionButtons', function (options) {
const hash = options && options.hash ? options.hash : {};
const formId = String(hash.formId || '').trim();
const saveLabel = String(hash.saveLabel || 'Save');
const saveButtonClass = String(hash.saveButtonClass || 'btn btn-success');
const saveActionName = String(hash.saveActionName || 'save_action');
const saveActionValue = String(hash.saveActionValue || 'save');
const closeLabel = String(hash.saveAndCloseLabel || 'Save and Close');
const closeValue = String(hash.saveAndCloseValue || 'close');
const newLabel = String(hash.saveAndNewLabel || 'Save and New');
const newValue = String(hash.saveAndNewValue || 'new');
const showSaveAndClose = hash.showSaveAndClose === undefined ? true : String(hash.showSaveAndClose).toLowerCase() !== 'false';
const showSaveAndNew = hash.showSaveAndNew === undefined ? true : String(hash.showSaveAndNew).toLowerCase() !== 'false';
const ariaLabel = String(hash.ariaLabel || 'Save actions');
const hasSecondaryActions = showSaveAndClose || showSaveAndNew;
if (!formId) {
return '';
}
const escape = Handlebars.escapeExpression;
const formAttr = ` form="${escape(formId)}"`;
return new Handlebars.SafeString([
'<div class="btn-group save-action-group" role="group">',
`<button type="submit" class="${escape(saveButtonClass)}"${formAttr} name="${escape(saveActionName)}" value="${escape(saveActionValue)}">${escape(saveLabel)}</button>`,
hasSecondaryActions ? `<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"><span class="visually-hidden">${escape(ariaLabel)}</span></button>` : '',
hasSecondaryActions ? '<div class="dropdown-menu dropdown-menu-end">' : '',
hasSecondaryActions && showSaveAndClose ? `<button type="submit" class="dropdown-item"${formAttr} name="${escape(saveActionName)}" value="${escape(closeValue)}">${escape(closeLabel)}</button>` : '',
hasSecondaryActions && showSaveAndNew ? `<button type="submit" class="dropdown-item"${formAttr} name="${escape(saveActionName)}" value="${escape(newValue)}">${escape(newLabel)}</button>` : '',
hasSecondaryActions ? '</div>' : '',
'</div>',
].join(''));
});
}
// Shared partials are registered once at startup.
function registerPartials(Handlebars, viewsRoot) {
Handlebars.registerPartial('modal-shell', fs.readFileSync(path.join(viewsRoot, 'shared', 'modal-shell.hbs'), 'utf8'));
Handlebars.registerPartial('table-pagination', fs.readFileSync(path.join(viewsRoot, 'shared', 'table', 'table-pagination.hbs'), 'utf8'));
}
// One entry point keeps Handlebars bootstrap centralized.
function registerHandlebars(Handlebars, deps) {
const viewsRoot = String(deps && deps.viewsRoot || '').trim();
const hasPermission = deps && deps.hasPermission;
const hasAnyPermission = deps && deps.hasAnyPermission;
if (!Handlebars || !viewsRoot || typeof hasPermission !== 'function' || typeof hasAnyPermission !== 'function') {
throw new Error('registerHandlebars requires Handlebars, viewsRoot, hasPermission, and hasAnyPermission.');
}
registerGeneralHelpers(Handlebars);
registerUrlHelpers(Handlebars);
registerPermissionHelpers(Handlebars, { hasPermission: hasPermission, hasAnyPermission: hasAnyPermission });
registerUserHelpers(Handlebars);
registerActionHelpers(Handlebars);
registerPartials(Handlebars, viewsRoot);
}
module.exports = {
registerHandlebars: registerHandlebars
};