Release v2.5.6

This commit is contained in:
2026-08-05 19:38:16 +01:00
parent a8ef35b287
commit 9f831f04bc
161 changed files with 8487 additions and 1295 deletions
@@ -9,6 +9,12 @@ const TASKS_PER_PAGE = 25;
const QUEUE_PAGE_URL = '/settings/tasks-background';
const SCHEDULED_PAGE_URL = '/settings/tasks-scheduled';
const DATE_FILTER_FIELDS = {
createdAt: 'queued',
startedAt: 'started',
finishedAt: 'finished'
};
function formatIntervalLabel(intervalMs) {
const value = Math.max(1, Number(intervalMs) || 0);
if (value % 60000 === 0) {
@@ -36,6 +42,54 @@ function normalizeSourceFilter(sourceType, sourceId) {
};
}
function normalizeDateInput(value) {
const normalizedValue = String(value || '').trim();
return /^\d{4}-\d{2}-\d{2}$/.test(normalizedValue) ? normalizedValue : '';
}
function normalizeDateField(value) {
const normalizedField = String(value || '').trim();
return DATE_FILTER_FIELDS[normalizedField] ? normalizedField : 'createdAt';
}
function getIsoDateString(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '';
}
return date.toISOString().slice(0, 10);
}
function getTaskDateValue(task, dateField) {
if (!task || !dateField) {
return '';
}
return task[dateField] || '';
}
function taskMatchesDateFilter(task, dateFilter) {
if (!dateFilter || (!dateFilter.from && !dateFilter.to)) {
return true;
}
const taskDate = getIsoDateString(getTaskDateValue(task, dateFilter.field));
if (!taskDate) {
return false;
}
if (dateFilter.from && taskDate < dateFilter.from) {
return false;
}
if (dateFilter.to && taskDate > dateFilter.to) {
return false;
}
return true;
}
function taskMatchesFilters(task, sourceFilter) {
if (!sourceFilter || !sourceFilter.sourceType || !sourceFilter.sourceId) {
return true;
@@ -107,12 +161,14 @@ function buildQueuePageViewModel(data, message, currentUser) {
queryState.sourceType = sourceFilter.sourceType;
queryState.sourceId = sourceFilter.sourceId;
const filteredTasks = tasks.filter(function (task) {
return taskMatchesFilters(task, sourceFilter) && taskMatchesSearch(queryState.search, [
return taskMatchesFilters(task, sourceFilter) && taskMatchesDateFilter(task, { field: 'createdAt', from: '', to: '' }) && taskMatchesSearch(queryState.search, [
'title',
'key',
'category',
'status',
'errorMessage',
'createdAt',
'startedAt',
'finishedAt',
function (item) { return item && item.metadata && item.metadata.sourceName; },
function (item) { return item && item.metadata && item.metadata.sourceType; },
function (item) { return item && item.metadata && item.metadata.sourceId; }
@@ -199,6 +255,11 @@ function buildQueuePageViewModel(data, message, currentUser) {
summary: summary,
statusCards: statusCards,
activeSourceLabel: queryState.sourceType && queryState.sourceId ? `${queryState.sourceType} #${queryState.sourceId}` : '',
search: queryState.search,
sort: sort,
direction: direction,
sourceType: queryState.sourceType,
sourceId: queryState.sourceId,
clearFiltersUrl: QUEUE_PAGE_URL,
hasActiveFilters: hasActiveFilters,
pagination: pagination,
+3 -1
View File
@@ -77,6 +77,8 @@ module.exports = function registerFontRoutes(app, deps) {
const backgroundTaskQueue = deps.backgroundTaskQueue;
const setAuthMessageCookie = deps.setAuthMessageCookie;
const FONT_FAMILY_MAX_LENGTH = 255;
if (!pool || !pages || !upload || !uploadDir) {
throw new Error('registerFontRoutes requires pool, pages, upload, and uploadDir.');
}
@@ -106,7 +108,7 @@ module.exports = function registerFontRoutes(app, deps) {
return res.status(400).send('No font file was uploaded.');
}
const result = await fontLibrary.addFontFile(uploadDir, req.file, String(req.body && req.body.font_family || '').trim());
const result = await fontLibrary.addFontFile(uploadDir, req.file, String(req.body && req.body.font_family || '').trim().slice(0, FONT_FAMILY_MAX_LENGTH));
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTask === 'function') {
await backgroundTaskQueue.enqueueTask({
key: `font-sync:${result && result.font && result.font.id ? result.font.id : Date.now()}`,
+20
View File
@@ -0,0 +1,20 @@
// RBAC role duplicate helpers.
function buildDuplicateRoleName(roleName) {
const source = String(roleName || 'Role').trim() || 'Role';
return 'Copy of ' + source;
}
function buildDuplicateRole(role, duplicateName) {
const source = role || {};
return {
id: null,
name: String(duplicateName || buildDuplicateRoleName(source.name)).trim(),
description: String(source.description || '').trim()
};
}
module.exports = {
buildDuplicateRoleName,
buildDuplicateRole
};
@@ -1,5 +1,81 @@
// Shared RBAC form view-model helpers.
function normalizeSectionId(value) {
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');
}
function buildPermissionSections(permissionGroups) {
const sections = [];
const sectionIndex = new Map();
const actionDefinitions = [
{ key: 'read', label: 'Read' },
{ key: 'create', label: 'Create' },
{ key: 'update', label: 'Update' },
{ key: 'delete', label: 'Delete' },
{ key: 'allow', label: 'Allow' }
];
(Array.isArray(permissionGroups) ? permissionGroups : []).forEach(function (group) {
const sectionTitle = String(group && group.categoryName || 'Permissions').trim() || 'Permissions';
const sectionKey = normalizeSectionId(sectionTitle) || 'permissions';
if (!sectionIndex.has(sectionKey)) {
sectionIndex.set(sectionKey, {
id: sectionKey,
title: sectionTitle,
sectionOrder: Number(group && group.sectionOrder) || 999,
groups: []
});
sections.push(sectionIndex.get(sectionKey));
}
const section = sectionIndex.get(sectionKey);
const groupPermissions = Array.isArray(group && group.permissions) ? group.permissions.slice() : [];
const sectionGroup = Object.assign({}, group, {
permissions: groupPermissions
});
section.groups.push(sectionGroup);
});
sections.forEach(function (section) {
section.actions = actionDefinitions.map(function (actionDefinition) {
return {
key: actionDefinition.key,
label: actionDefinition.label
};
});
section.groups.sort(function (left, right) {
const leftOrder = Number(left.resourceOrder) || 999;
const rightOrder = Number(right.resourceOrder) || 999;
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
return String(left.title || '').localeCompare(String(right.title || ''));
});
section.groups.forEach(function (group) {
group.cells = section.actions.map(function (action) {
return (Array.isArray(group.permissions) ? group.permissions : []).find(function (permission) {
return String(permission.actionKey || '').trim().toLowerCase() === action.key;
}) || null;
});
});
});
sections.sort(function (left, right) {
const leftOrder = Number(left.sectionOrder) || 999;
const rightOrder = Number(right.sectionOrder) || 999;
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
return String(left.title || '').localeCompare(String(right.title || ''));
});
return sections;
}
function buildRbacAddViewModel(message, currentUser, formValues, permissionGroups, users, pagination, messageVariant) {
return {
title: 'Create role',
@@ -19,11 +95,12 @@ function buildRbacAddViewModel(message, currentUser, formValues, permissionGroup
currentUser: currentUser || null,
formValues: formValues || {},
permissionGroups: permissionGroups || [],
permissionSections: buildPermissionSections(permissionGroups),
users: users || [],
pagination: pagination || null,
roleEditBasePath: '/rbac/new',
scripts: ['js/rbac-permissions.js'],
formAttrs: 'data-async-save data-async-save-close-url="/rbac" data-async-save-new-redirect="response-url"'
formAttrs: 'data-async-save data-async-save-close-url="/rbac" data-async-save-new-redirect="response-url" data-async-save-new-url="/rbac/new"'
};
}
@@ -46,10 +123,12 @@ function buildRbacEditViewModel(role, message, currentUser, permissionGroups, us
role: role,
inUse: Boolean(role && role.inUse),
permissionGroups: permissionGroups || [],
permissionSections: buildPermissionSections(permissionGroups),
users: users || [],
pagination: pagination || null,
roleEditBasePath: '/rbac/' + Number(role && role.id) + '/edit',
scripts: ['js/rbac-permissions.js']
scripts: ['js/rbac-permissions.js'],
formAttrs: 'data-async-save data-async-save-close-url="/rbac" data-async-save-new-url="/rbac/new"'
};
}
@@ -0,0 +1,20 @@
// User duplicate helpers.
function buildDuplicateUserName(name) {
const source = String(name || 'User').trim() || 'User';
return 'Copy of ' + source;
}
function buildDuplicateUser(user, duplicateName, duplicateUsername) {
const source = user || {};
return {
id: null,
name: String(duplicateName || buildDuplicateUserName(source.name)).trim(),
username: String(duplicateUsername || '').trim()
};
}
module.exports = {
buildDuplicateUserName,
buildDuplicateUser
};
@@ -32,7 +32,7 @@ function buildUsersAddViewModel(message, currentUser, roles, formValues, message
footerDeleteTitle: 'Delete is available after the user has been created.',
footerShowDelete: true,
message: message,
messageVariant: messageVariant || 'primary',
messageVariant: messageVariant || 'success',
currentUser: currentUser || null,
roles: mapRolesForForm(roles, []),
formValues: formValues || {},
@@ -55,11 +55,13 @@ function buildUsersEditViewModel(user, message, currentUser, roles) {
footerDeleteConfirmMessage: 'Delete this user?',
footerDeleteTitle: user && user.inUse ? 'Delete is disabled while this user is the only account left.' : '',
footerShowDelete: true,
showSaveSecondaryActions: true,
message: message,
currentUser: currentUser || null,
user: user,
inUse: Boolean(user && user.inUse),
roles: mapRolesForForm(roles, user && user.roleIds)
roles: mapRolesForForm(roles, user && user.roleIds),
formAttrs: 'data-async-save data-async-save-close-url="/users" data-async-save-new-url="/users/new"'
};
}