Release v2.5.3
This commit is contained in:
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
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.5.3 - 2026-08-03
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The user edit page delete action now shows a confirmation prompt before removing an account.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Schedule modal cancel now restores the original rule set instead of keeping edits made after the modal opened.
|
||||||
|
|
||||||
## 2.5.2 - 2026-08-03
|
## 2.5.2 - 2026-08-03
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.5.2",
|
"version": "2.5.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -210,6 +210,15 @@
|
|||||||
return savedMessage;
|
return savedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseAsyncSaveErrorMessage(responseText) {
|
||||||
|
var errorMessage = parseAsyncSaveResponseMessage(responseText);
|
||||||
|
if (errorMessage) {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(responseText || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
function getAsyncSaveActionField(form) {
|
function getAsyncSaveActionField(form) {
|
||||||
if (!form || !form.querySelector) {
|
if (!form || !form.querySelector) {
|
||||||
return null;
|
return null;
|
||||||
@@ -302,7 +311,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(await response.text() || (settings.errorMessage || 'Unable to save changes.'));
|
var responseText = await response.text();
|
||||||
|
throw new Error(parseAsyncSaveErrorMessage(responseText) || (settings.errorMessage || 'Unable to save changes.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoginRedirect(response)) {
|
if (isLoginRedirect(response)) {
|
||||||
|
|||||||
@@ -478,9 +478,18 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeScheduleModal() {
|
function closeScheduleModal(options) {
|
||||||
var dialog = document.getElementById('slide-schedule-dialog');
|
var dialog = document.getElementById('slide-schedule-dialog');
|
||||||
var content = document.getElementById('slide-schedule-content');
|
var content = document.getElementById('slide-schedule-content');
|
||||||
|
var shouldDiscardDraft = Boolean(options && options.discardDraft);
|
||||||
|
var shouldPreserveChanges = Boolean(options && options.preserveChanges);
|
||||||
|
|
||||||
|
if (shouldDiscardDraft && !shouldPreserveChanges && typeof window.restorePlaylistScheduleDraft === 'function') {
|
||||||
|
window.restorePlaylistScheduleDraft();
|
||||||
|
}
|
||||||
|
if (shouldDiscardDraft && typeof window.clearPlaylistScheduleDraft === 'function') {
|
||||||
|
window.clearPlaylistScheduleDraft();
|
||||||
|
}
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
content.innerHTML = '';
|
content.innerHTML = '';
|
||||||
@@ -629,6 +638,7 @@
|
|||||||
var isDraftMode = Boolean(form.getAttribute('data-schedule-draft') === 'true');
|
var isDraftMode = Boolean(form.getAttribute('data-schedule-draft') === 'true');
|
||||||
var rowKeyInput = form.querySelector('[name="row_key"]');
|
var rowKeyInput = form.querySelector('[name="row_key"]');
|
||||||
var draftStore = window.__playlistScheduleDraftStore = window.__playlistScheduleDraftStore || {};
|
var draftStore = window.__playlistScheduleDraftStore = window.__playlistScheduleDraftStore || {};
|
||||||
|
var draftSnapshotStore = window.__playlistScheduleDraftSnapshotStore = window.__playlistScheduleDraftSnapshotStore || {};
|
||||||
var cancelButton = scope.querySelector('[data-schedule-cancel]');
|
var cancelButton = scope.querySelector('[data-schedule-cancel]');
|
||||||
|
|
||||||
// Draft state is keyed by the row being edited so cancel/save can restore it later.
|
// Draft state is keyed by the row being edited so cancel/save can restore it later.
|
||||||
@@ -659,6 +669,41 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function captureScheduleDraftState() {
|
||||||
|
return getRuleCards().map(function (card) {
|
||||||
|
return {
|
||||||
|
rule: readRuleDraftFromCard(card),
|
||||||
|
isCollapsed: Boolean(card && card.classList && card.classList.contains('collapsed-card'))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderScheduleDraftState(ruleStates) {
|
||||||
|
var states = Array.isArray(ruleStates) ? ruleStates : [];
|
||||||
|
|
||||||
|
scheduleRuleList.innerHTML = '';
|
||||||
|
states.forEach(function (ruleState, index) {
|
||||||
|
var card = createRuleCardElement(ruleState && ruleState.rule ? ruleState.rule : {}, index + 1, { isCollapsed: Boolean(ruleState && ruleState.isCollapsed) });
|
||||||
|
|
||||||
|
if (card) {
|
||||||
|
scheduleRuleList.appendChild(card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
refreshRuleNumbers();
|
||||||
|
syncScheduleRulesField();
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreScheduleDraft() {
|
||||||
|
var draftKey = getScheduleDraftKey();
|
||||||
|
var snapshot = draftKey && Object.prototype.hasOwnProperty.call(draftSnapshotStore, draftKey) ? draftSnapshotStore[draftKey] : null;
|
||||||
|
|
||||||
|
if (!draftKey || !Array.isArray(snapshot)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderScheduleDraftState(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
function getScheduleDraftRules() {
|
function getScheduleDraftRules() {
|
||||||
var draftKey = getScheduleDraftKey();
|
var draftKey = getScheduleDraftKey();
|
||||||
if (!draftKey || !Object.prototype.hasOwnProperty.call(draftStore, draftKey)) {
|
if (!draftKey || !Object.prototype.hasOwnProperty.call(draftStore, draftKey)) {
|
||||||
@@ -687,9 +732,18 @@
|
|||||||
if (draftKey && Object.prototype.hasOwnProperty.call(draftStore, draftKey)) {
|
if (draftKey && Object.prototype.hasOwnProperty.call(draftStore, draftKey)) {
|
||||||
delete draftStore[draftKey];
|
delete draftStore[draftKey];
|
||||||
}
|
}
|
||||||
|
if (draftKey && Object.prototype.hasOwnProperty.call(draftSnapshotStore, draftKey)) {
|
||||||
|
delete draftSnapshotStore[draftKey];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.clearPlaylistScheduleDraft = clearScheduleDraft;
|
window.clearPlaylistScheduleDraft = clearScheduleDraft;
|
||||||
|
window.restorePlaylistScheduleDraft = restoreScheduleDraft;
|
||||||
|
|
||||||
|
var draftKey = getScheduleDraftKey();
|
||||||
|
if (draftKey && !Object.prototype.hasOwnProperty.call(draftSnapshotStore, draftKey)) {
|
||||||
|
draftSnapshotStore[draftKey] = captureScheduleDraftState();
|
||||||
|
}
|
||||||
|
|
||||||
function getRuleCards() {
|
function getRuleCards() {
|
||||||
return Array.prototype.slice.call(scheduleRuleList.querySelectorAll('[data-schedule-rule-card]'));
|
return Array.prototype.slice.call(scheduleRuleList.querySelectorAll('[data-schedule-rule-card]'));
|
||||||
@@ -1047,11 +1101,7 @@
|
|||||||
|
|
||||||
var draftRules = getScheduleDraftRules();
|
var draftRules = getScheduleDraftRules();
|
||||||
if (draftRules !== null) {
|
if (draftRules !== null) {
|
||||||
scheduleRuleList.innerHTML = '';
|
renderScheduleDraftState(draftRules);
|
||||||
draftRules.forEach(function (rule) {
|
|
||||||
addRule(rule && rule.rule ? rule.rule : {}, { isCollapsed: Boolean(rule && rule.isCollapsed) });
|
|
||||||
});
|
|
||||||
syncScheduleRulesField();
|
|
||||||
} else if (!getRuleCards().length) {
|
} else if (!getRuleCards().length) {
|
||||||
syncScheduleRulesField();
|
syncScheduleRulesField();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -434,8 +434,12 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPreviewPopupHash() {
|
||||||
|
return encodeURIComponent(JSON.stringify(buildPreviewPopupPayload()));
|
||||||
|
}
|
||||||
|
|
||||||
function buildPreviewPopupUrl() {
|
function buildPreviewPopupUrl() {
|
||||||
return '/slides/popup-preview#' + encodeURIComponent(JSON.stringify(buildPreviewPopupPayload()));
|
return '/slides/popup-preview#' + buildPreviewPopupHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncPopupPreview() {
|
function syncPopupPreview() {
|
||||||
@@ -444,7 +448,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
previewPopupWindow = window.open(buildPreviewPopupUrl(), 'slide-preview-popup', previewPopupFeatures);
|
previewPopupWindow.location.hash = buildPreviewPopupHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPreviewPopup() {
|
function openPreviewPopup() {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ function buildUsersEditViewModel(user, message, currentUser, roles) {
|
|||||||
footerCancelUrl: '/users',
|
footerCancelUrl: '/users',
|
||||||
footerDeleteUrl: '/users/' + user.id + '/delete',
|
footerDeleteUrl: '/users/' + user.id + '/delete',
|
||||||
footerDeleteDisabled: Boolean(user && user.inUse),
|
footerDeleteDisabled: Boolean(user && user.inUse),
|
||||||
|
footerDeleteConfirmMessage: 'Delete this user?',
|
||||||
footerDeleteTitle: user && user.inUse ? 'Delete is disabled while this user is the only account left.' : '',
|
footerDeleteTitle: user && user.inUse ? 'Delete is disabled while this user is the only account left.' : '',
|
||||||
footerShowDelete: true,
|
footerShowDelete: true,
|
||||||
message: message,
|
message: message,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-footer d-flex justify-content-end">
|
<div class="card-footer d-flex justify-content-end">
|
||||||
<div class="btn-group" role="group" aria-label="{{#if isEdit}}Profile actions{{else}}User actions{{/if}}">
|
<div class="btn-group" role="group" aria-label="{{#if isEdit}}Profile actions{{else}}User actions{{/if}}">
|
||||||
{{{saveActionButtons formId=formId saveUrl=footerSaveUrl cancelUrl=footerCancelUrl deleteUrl=footerDeleteUrl deleteDisabled=footerDeleteDisabled deleteTitle=footerDeleteTitle showSaveAndClose=false showSaveAndNew=false showDelete=footerShowDelete}}}
|
{{{saveActionButtons formId=formId saveUrl=footerSaveUrl cancelUrl=footerCancelUrl deleteUrl=footerDeleteUrl deleteConfirmMessage=footerDeleteConfirmMessage deleteDisabled=footerDeleteDisabled deleteTitle=footerDeleteTitle showSaveAndClose=false showSaveAndNew=false showDelete=footerShowDelete}}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const { buildUsersEditViewModel, buildUsersAddViewModel } = require('../src/web/routes/settings/users/form-view-model');
|
||||||
|
|
||||||
|
test('user edit view model includes delete confirmation text', () => {
|
||||||
|
const model = buildUsersEditViewModel({ id: 12, inUse: false, roleIds: [] }, 'Saved.', { id: 1 }, []);
|
||||||
|
|
||||||
|
assert.equal(model.footerDeleteUrl, '/users/12/delete');
|
||||||
|
assert.equal(model.footerDeleteConfirmMessage, 'Delete this user?');
|
||||||
|
assert.equal(model.footerDeleteDisabled, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('user add view model keeps delete disabled without a confirm message', () => {
|
||||||
|
const model = buildUsersAddViewModel('Saved.', { id: 1 }, [], { name: 'New User', username: 'newuser' }, 'primary');
|
||||||
|
|
||||||
|
assert.equal(model.footerDeleteDisabled, true);
|
||||||
|
assert.equal(model.footerDeleteUrl, '');
|
||||||
|
assert.equal(model.footerDeleteConfirmMessage, undefined);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user