Release v2.6.11
This commit is contained in:
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## 2.6.11 - 2026-08-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Role edits now save selected permissions from the shared RBAC form, so changes on the role page persist when you submit the form.
|
||||||
|
- RBAC permission sections now stay open independently in the accordion, so opening one section no longer closes the others.
|
||||||
|
|
||||||
## 2.6.10 - 2026-08-08
|
## 2.6.10 - 2026-08-08
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.6.10",
|
"version": "2.6.11",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
+7
-4
@@ -194,18 +194,21 @@ const PERMISSION_SECTIONS = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const PERMISSIONS = PERMISSION_SECTIONS.flatMap(function (section) {
|
const PERMISSIONS = PERMISSION_SECTIONS.flatMap(function (section, sectionIndex) {
|
||||||
return (Array.isArray(section.permissions) ? section.permissions : []).flatMap(function (resource) {
|
return (Array.isArray(section.permissions) ? section.permissions : []).flatMap(function (resource, resourceIndex) {
|
||||||
return (Array.isArray(resource.permissions) ? resource.permissions : []).map(function (action, index) {
|
return (Array.isArray(resource.permissions) ? resource.permissions : []).map(function (action, actionIndex) {
|
||||||
return {
|
return {
|
||||||
key: `${resource.key}.${action.key}`,
|
key: `${resource.key}.${action.key}`,
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
sectionOrder: section.order,
|
sectionOrder: section.order,
|
||||||
|
sectionIndex: sectionIndex,
|
||||||
actionName: action.name,
|
actionName: action.name,
|
||||||
permissionOrder: index + 1,
|
permissionOrder: actionIndex + 1,
|
||||||
|
permissionIndex: actionIndex,
|
||||||
sectionName: section.sectionName,
|
sectionName: section.sectionName,
|
||||||
resourceKey: resource.key,
|
resourceKey: resource.key,
|
||||||
resourceOrder: resource.order,
|
resourceOrder: resource.order,
|
||||||
|
resourceIndex: resourceIndex,
|
||||||
resourceName: resource.name,
|
resourceName: resource.name,
|
||||||
actionKey: action.key,
|
actionKey: action.key,
|
||||||
description: action.description
|
description: action.description
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
|
|
||||||
const LIST_PAGE_SIZE = 25;
|
const LIST_PAGE_SIZE = 25;
|
||||||
|
|
||||||
|
function toSortIndex(value) {
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) ? number : 999;
|
||||||
|
}
|
||||||
|
|
||||||
function slugifyRoleKey(name) {
|
function slugifyRoleKey(name) {
|
||||||
const value = String(name || '').trim().toLowerCase();
|
const value = String(name || '').trim().toLowerCase();
|
||||||
const slug = value.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, ROLE_KEY_MAX_LENGTH);
|
const slug = value.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, ROLE_KEY_MAX_LENGTH);
|
||||||
@@ -73,8 +78,11 @@
|
|||||||
resourceKey: definition ? definition.resourceKey : String(permission.section_name || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
resourceKey: definition ? definition.resourceKey : String(permission.section_name || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||||
resourceName: definition ? definition.resourceName : permission.section_name,
|
resourceName: definition ? definition.resourceName : permission.section_name,
|
||||||
categoryName: definition ? definition.sectionName : permission.section_name,
|
categoryName: definition ? definition.sectionName : permission.section_name,
|
||||||
|
sectionIndex: definition ? toSortIndex(definition.sectionIndex) : 999,
|
||||||
sectionOrder: definition ? definition.sectionOrder : 999,
|
sectionOrder: definition ? definition.sectionOrder : 999,
|
||||||
|
resourceIndex: definition ? toSortIndex(definition.resourceIndex) : 999,
|
||||||
resourceOrder: definition ? Number(definition.resourceOrder) || 999 : 999,
|
resourceOrder: definition ? Number(definition.resourceOrder) || 999 : 999,
|
||||||
|
permissionIndex: definition ? toSortIndex(definition.permissionIndex) : 999,
|
||||||
permissionOrder: definition ? Number(definition.permissionOrder) || 999 : 999,
|
permissionOrder: definition ? Number(definition.permissionOrder) || 999 : 999,
|
||||||
actionKey: definition ? definition.actionKey : 'read',
|
actionKey: definition ? definition.actionKey : 'read',
|
||||||
actionLabel: definition ? definition.actionName : getActionLabel(permission.actionKey),
|
actionLabel: definition ? definition.actionName : getActionLabel(permission.actionKey),
|
||||||
@@ -103,7 +111,9 @@
|
|||||||
id: sectionKey || 'permissions',
|
id: sectionKey || 'permissions',
|
||||||
title: String(permission.resourceName || permission.categoryName || 'Permissions').trim(),
|
title: String(permission.resourceName || permission.categoryName || 'Permissions').trim(),
|
||||||
categoryName: String(permission.categoryName || '').trim(),
|
categoryName: String(permission.categoryName || '').trim(),
|
||||||
|
sectionIndex: toSortIndex(permission.sectionIndex),
|
||||||
sectionOrder: Number(permission.sectionOrder) || 999,
|
sectionOrder: Number(permission.sectionOrder) || 999,
|
||||||
|
resourceIndex: toSortIndex(permission.resourceIndex),
|
||||||
permissions: []
|
permissions: []
|
||||||
};
|
};
|
||||||
groupIndex.set(sectionKey, group);
|
groupIndex.set(sectionKey, group);
|
||||||
@@ -114,8 +124,13 @@
|
|||||||
|
|
||||||
groups.forEach(function (group) {
|
groups.forEach(function (group) {
|
||||||
group.permissions.sort(function (left, right) {
|
group.permissions.sort(function (left, right) {
|
||||||
const leftOrder = Number(left.permissionOrder) || 999;
|
const leftIndex = toSortIndex(left.permissionIndex);
|
||||||
const rightOrder = Number(right.permissionOrder) || 999;
|
const rightIndex = toSortIndex(right.permissionIndex);
|
||||||
|
if (leftIndex !== rightIndex) {
|
||||||
|
return leftIndex - rightIndex;
|
||||||
|
}
|
||||||
|
const leftOrder = toSortIndex(left.permissionOrder);
|
||||||
|
const rightOrder = toSortIndex(right.permissionOrder);
|
||||||
if (leftOrder !== rightOrder) {
|
if (leftOrder !== rightOrder) {
|
||||||
return leftOrder - rightOrder;
|
return leftOrder - rightOrder;
|
||||||
}
|
}
|
||||||
@@ -124,8 +139,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
groups.sort(function (left, right) {
|
groups.sort(function (left, right) {
|
||||||
const leftOrder = Number(left.sectionOrder) || 999;
|
const leftIndex = toSortIndex(left.sectionIndex);
|
||||||
const rightOrder = Number(right.sectionOrder) || 999;
|
const rightIndex = toSortIndex(right.sectionIndex);
|
||||||
|
if (leftIndex !== rightIndex) {
|
||||||
|
return leftIndex - rightIndex;
|
||||||
|
}
|
||||||
|
const leftOrder = toSortIndex(left.sectionOrder);
|
||||||
|
const rightOrder = toSortIndex(right.sectionOrder);
|
||||||
if (leftOrder !== rightOrder) {
|
if (leftOrder !== rightOrder) {
|
||||||
return leftOrder - rightOrder;
|
return leftOrder - rightOrder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ function normalizeSectionId(value) {
|
|||||||
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toSortIndex(value) {
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) ? number : 999;
|
||||||
|
}
|
||||||
|
|
||||||
function buildPermissionSections(permissionGroups) {
|
function buildPermissionSections(permissionGroups) {
|
||||||
const sections = [];
|
const sections = [];
|
||||||
const sectionIndex = new Map();
|
const sectionIndex = new Map();
|
||||||
@@ -23,7 +28,8 @@ function buildPermissionSections(permissionGroups) {
|
|||||||
sectionIndex.set(sectionKey, {
|
sectionIndex.set(sectionKey, {
|
||||||
id: sectionKey,
|
id: sectionKey,
|
||||||
title: sectionTitle,
|
title: sectionTitle,
|
||||||
sectionOrder: Number(group && group.sectionOrder) || 999,
|
sourceIndex: toSortIndex(group && group.sectionIndex),
|
||||||
|
sectionOrder: toSortIndex(group && group.sectionOrder),
|
||||||
groups: []
|
groups: []
|
||||||
});
|
});
|
||||||
sections.push(sectionIndex.get(sectionKey));
|
sections.push(sectionIndex.get(sectionKey));
|
||||||
@@ -47,8 +53,14 @@ function buildPermissionSections(permissionGroups) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
section.groups.sort(function (left, right) {
|
section.groups.sort(function (left, right) {
|
||||||
const leftOrder = Number(left.resourceOrder) || 999;
|
const leftIndex = toSortIndex(left.resourceIndex);
|
||||||
const rightOrder = Number(right.resourceOrder) || 999;
|
const rightIndex = toSortIndex(right.resourceIndex);
|
||||||
|
if (leftIndex !== rightIndex) {
|
||||||
|
return leftIndex - rightIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftOrder = toSortIndex(left.resourceOrder);
|
||||||
|
const rightOrder = toSortIndex(right.resourceOrder);
|
||||||
if (leftOrder !== rightOrder) {
|
if (leftOrder !== rightOrder) {
|
||||||
return leftOrder - rightOrder;
|
return leftOrder - rightOrder;
|
||||||
}
|
}
|
||||||
@@ -65,8 +77,14 @@ function buildPermissionSections(permissionGroups) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
sections.sort(function (left, right) {
|
sections.sort(function (left, right) {
|
||||||
const leftOrder = Number(left.sectionOrder) || 999;
|
const leftIndex = toSortIndex(left.sourceIndex);
|
||||||
const rightOrder = Number(right.sectionOrder) || 999;
|
const rightIndex = toSortIndex(right.sourceIndex);
|
||||||
|
if (leftIndex !== rightIndex) {
|
||||||
|
return leftIndex - rightIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftOrder = toSortIndex(left.sectionOrder);
|
||||||
|
const rightOrder = toSortIndex(right.sectionOrder);
|
||||||
if (leftOrder !== rightOrder) {
|
if (leftOrder !== rightOrder) {
|
||||||
return leftOrder - rightOrder;
|
return leftOrder - rightOrder;
|
||||||
}
|
}
|
||||||
@@ -134,5 +152,6 @@ function buildRbacEditViewModel(role, message, currentUser, permissionGroups, us
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
buildRbacAddViewModel: buildRbacAddViewModel,
|
buildRbacAddViewModel: buildRbacAddViewModel,
|
||||||
buildRbacEditViewModel: buildRbacEditViewModel
|
buildRbacEditViewModel: buildRbacEditViewModel,
|
||||||
|
buildPermissionSections: buildPermissionSections
|
||||||
};
|
};
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="{{formId}}" method="post" action="{{formAction}}" {{{formAttrs}}}>
|
<form id="{{formId}}" method="post" action="{{formAction}}" {{{formAttrs}}}>
|
||||||
|
<input type="hidden" name="permissions_present" value="1" />
|
||||||
<input type="hidden" name="users_present" value="1" />
|
<input type="hidden" name="users_present" value="1" />
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
@@ -94,7 +95,7 @@
|
|||||||
<span class="fw-semibold">{{title}}</span>
|
<span class="fw-semibold">{{title}}</span>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="permission-section-collapse-{{id}}" class="accordion-collapse collapse {{#if @first}}show{{/if}}" aria-labelledby="permission-section-heading-{{id}}" data-bs-parent="#role-permissions-accordion">
|
<div id="permission-section-collapse-{{id}}" class="accordion-collapse collapse {{#if @first}}show{{/if}}" aria-labelledby="permission-section-heading-{{id}}">
|
||||||
<div class="accordion-body p-0">
|
<div class="accordion-body p-0">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped align-middle mb-0 rbac-permissions-table" style="table-layout: fixed; width: 100%;">
|
<table class="table table-striped align-middle mb-0 rbac-permissions-table" style="table-layout: fixed; width: 100%;">
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ const fs = require('node:fs');
|
|||||||
|
|
||||||
const rbacPermissionsScript = fs.readFileSync(require.resolve('../src/web/public/js/rbac-permissions.js'), 'utf8');
|
const rbacPermissionsScript = fs.readFileSync(require.resolve('../src/web/public/js/rbac-permissions.js'), 'utf8');
|
||||||
const rbacFormTemplate = fs.readFileSync(require.resolve('../src/web/views/settings/rbac/form.hbs'), 'utf8');
|
const rbacFormTemplate = fs.readFileSync(require.resolve('../src/web/views/settings/rbac/form.hbs'), 'utf8');
|
||||||
|
const { buildPermissionSections } = require('../src/web/routes/settings/rbac/form-view-model');
|
||||||
const rbacFormViewModel = fs.readFileSync(require.resolve('../src/web/routes/settings/rbac/form-view-model.js'), 'utf8');
|
const rbacFormViewModel = fs.readFileSync(require.resolve('../src/web/routes/settings/rbac/form-view-model.js'), 'utf8');
|
||||||
const rbacSource = fs.readFileSync(require.resolve('../src/rbac.js'), 'utf8');
|
const rbacSource = fs.readFileSync(require.resolve('../src/rbac.js'), 'utf8');
|
||||||
|
|
||||||
test('rbac form exposes bulk permission controls', () => {
|
test('rbac form exposes bulk permission controls', () => {
|
||||||
|
assert.match(rbacFormTemplate, /name="permissions_present" value="1"/);
|
||||||
assert.match(rbacFormTemplate, /accordion accordion-flush/);
|
assert.match(rbacFormTemplate, /accordion accordion-flush/);
|
||||||
assert.match(rbacFormTemplate, /card card-outline card-secondary overflow-hidden/);
|
assert.match(rbacFormTemplate, /card card-outline card-secondary overflow-hidden/);
|
||||||
assert.match(rbacFormTemplate, /accordion-item overflow-hidden" data-permission-section/);
|
assert.match(rbacFormTemplate, /accordion-item overflow-hidden" data-permission-section/);
|
||||||
assert.match(rbacFormTemplate, /accordion-body p-0/);
|
assert.match(rbacFormTemplate, /accordion-body p-0/);
|
||||||
|
assert.ok(!rbacFormTemplate.includes('data-bs-parent="#role-permissions-accordion"'));
|
||||||
assert.match(rbacFormTemplate, /table-layout: fixed; width: 100%;/);
|
assert.match(rbacFormTemplate, /table-layout: fixed; width: 100%;/);
|
||||||
assert.match(rbacFormTemplate, /<col style="width: 5\.5rem;" \/>/);
|
assert.match(rbacFormTemplate, /<col style="width: 5\.5rem;" \/>/);
|
||||||
assert.match(rbacFormTemplate, /class="text-center"/);
|
assert.match(rbacFormTemplate, /class="text-center"/);
|
||||||
@@ -51,6 +54,45 @@ test('rbac permissions script supports bulk permission selection', () => {
|
|||||||
assert.ok(!rbacPermissionsScript.includes('data-permission-section-select-none'));
|
assert.ok(!rbacPermissionsScript.includes('data-permission-section-select-none'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rbac accordion sections follow source order', () => {
|
||||||
|
const sections = buildPermissionSections([
|
||||||
|
{
|
||||||
|
categoryName: 'Settings',
|
||||||
|
sectionIndex: 3,
|
||||||
|
sectionOrder: 40,
|
||||||
|
resourceIndex: 1,
|
||||||
|
resourceOrder: 20,
|
||||||
|
title: 'Later resource',
|
||||||
|
permissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoryName: 'Main navigation',
|
||||||
|
sectionIndex: 0,
|
||||||
|
sectionOrder: 10,
|
||||||
|
resourceIndex: 1,
|
||||||
|
resourceOrder: 20,
|
||||||
|
title: 'Clients',
|
||||||
|
permissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoryName: 'Main navigation',
|
||||||
|
sectionIndex: 0,
|
||||||
|
sectionOrder: 10,
|
||||||
|
resourceIndex: 0,
|
||||||
|
resourceOrder: 10,
|
||||||
|
title: 'Dashboard',
|
||||||
|
permissions: []
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(sections.map(function (section) {
|
||||||
|
return section.title;
|
||||||
|
}), ['Main navigation', 'Settings']);
|
||||||
|
assert.deepEqual(sections[0].groups.map(function (group) {
|
||||||
|
return group.title;
|
||||||
|
}), ['Dashboard', 'Clients']);
|
||||||
|
});
|
||||||
|
|
||||||
test('rbac duplicate route exists', () => {
|
test('rbac duplicate route exists', () => {
|
||||||
const rbacRoutes = fs.readFileSync(require.resolve('../src/web/routes/admin/rbac.js'), 'utf8');
|
const rbacRoutes = fs.readFileSync(require.resolve('../src/web/routes/admin/rbac.js'), 'utf8');
|
||||||
const duplicateHelpers = fs.readFileSync(require.resolve('../src/web/routes/settings/rbac/duplicate.js'), 'utf8');
|
const duplicateHelpers = fs.readFileSync(require.resolve('../src/web/routes/settings/rbac/duplicate.js'), 'utf8');
|
||||||
|
|||||||
Reference in New Issue
Block a user