From a5e8ecb21ff90d94a7eca3fc80003a1668b459ae Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Mon, 3 Aug 2026 20:04:43 +0100 Subject: [PATCH] Release v2.5.3 --- CHANGELOG.md | 10 +++ package.json | 2 +- src/web/public/js/admin/admin-page.js | 12 +++- .../public/js/playlists/playlist-schedule.js | 62 +++++++++++++++++-- src/web/public/js/slides/slide-form.js | 8 ++- .../routes/settings/users/form-view-model.js | 1 + src/web/views/settings/users/form.hbs | 2 +- test/users-form-view-model.test.js | 20 ++++++ 8 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 test/users-form-view-model.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2518733..5f125f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ 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 ### Changed diff --git a/package.json b/package.json index e7e52f2..169cd26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.5.2", + "version": "2.5.3", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/web/public/js/admin/admin-page.js b/src/web/public/js/admin/admin-page.js index a44ddb5..9f971e3 100644 --- a/src/web/public/js/admin/admin-page.js +++ b/src/web/public/js/admin/admin-page.js @@ -210,6 +210,15 @@ return savedMessage; } + function parseAsyncSaveErrorMessage(responseText) { + var errorMessage = parseAsyncSaveResponseMessage(responseText); + if (errorMessage) { + return errorMessage; + } + + return String(responseText || '').trim(); + } + function getAsyncSaveActionField(form) { if (!form || !form.querySelector) { return null; @@ -302,7 +311,8 @@ }); 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)) { diff --git a/src/web/public/js/playlists/playlist-schedule.js b/src/web/public/js/playlists/playlist-schedule.js index f72c53a..1cd2e00 100644 --- a/src/web/public/js/playlists/playlist-schedule.js +++ b/src/web/public/js/playlists/playlist-schedule.js @@ -478,9 +478,18 @@ }); } - function closeScheduleModal() { + function closeScheduleModal(options) { var dialog = document.getElementById('slide-schedule-dialog'); 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) { content.innerHTML = ''; @@ -629,6 +638,7 @@ var isDraftMode = Boolean(form.getAttribute('data-schedule-draft') === 'true'); var rowKeyInput = form.querySelector('[name="row_key"]'); var draftStore = window.__playlistScheduleDraftStore = window.__playlistScheduleDraftStore || {}; + var draftSnapshotStore = window.__playlistScheduleDraftSnapshotStore = window.__playlistScheduleDraftSnapshotStore || {}; var cancelButton = scope.querySelector('[data-schedule-cancel]'); // 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() { var draftKey = getScheduleDraftKey(); if (!draftKey || !Object.prototype.hasOwnProperty.call(draftStore, draftKey)) { @@ -687,9 +732,18 @@ if (draftKey && Object.prototype.hasOwnProperty.call(draftStore, draftKey)) { delete draftStore[draftKey]; } + if (draftKey && Object.prototype.hasOwnProperty.call(draftSnapshotStore, draftKey)) { + delete draftSnapshotStore[draftKey]; + } } window.clearPlaylistScheduleDraft = clearScheduleDraft; + window.restorePlaylistScheduleDraft = restoreScheduleDraft; + + var draftKey = getScheduleDraftKey(); + if (draftKey && !Object.prototype.hasOwnProperty.call(draftSnapshotStore, draftKey)) { + draftSnapshotStore[draftKey] = captureScheduleDraftState(); + } function getRuleCards() { return Array.prototype.slice.call(scheduleRuleList.querySelectorAll('[data-schedule-rule-card]')); @@ -1047,11 +1101,7 @@ var draftRules = getScheduleDraftRules(); if (draftRules !== null) { - scheduleRuleList.innerHTML = ''; - draftRules.forEach(function (rule) { - addRule(rule && rule.rule ? rule.rule : {}, { isCollapsed: Boolean(rule && rule.isCollapsed) }); - }); - syncScheduleRulesField(); + renderScheduleDraftState(draftRules); } else if (!getRuleCards().length) { syncScheduleRulesField(); } else { diff --git a/src/web/public/js/slides/slide-form.js b/src/web/public/js/slides/slide-form.js index 627eaed..1b4d0a5 100644 --- a/src/web/public/js/slides/slide-form.js +++ b/src/web/public/js/slides/slide-form.js @@ -434,8 +434,12 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev }; } + function buildPreviewPopupHash() { + return encodeURIComponent(JSON.stringify(buildPreviewPopupPayload())); + } + function buildPreviewPopupUrl() { - return '/slides/popup-preview#' + encodeURIComponent(JSON.stringify(buildPreviewPopupPayload())); + return '/slides/popup-preview#' + buildPreviewPopupHash(); } function syncPopupPreview() { @@ -444,7 +448,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev return; } - previewPopupWindow = window.open(buildPreviewPopupUrl(), 'slide-preview-popup', previewPopupFeatures); + previewPopupWindow.location.hash = buildPreviewPopupHash(); } function openPreviewPopup() { diff --git a/src/web/routes/settings/users/form-view-model.js b/src/web/routes/settings/users/form-view-model.js index 1780c36..3d4dd12 100644 --- a/src/web/routes/settings/users/form-view-model.js +++ b/src/web/routes/settings/users/form-view-model.js @@ -52,6 +52,7 @@ function buildUsersEditViewModel(user, message, currentUser, roles) { footerCancelUrl: '/users', footerDeleteUrl: '/users/' + user.id + '/delete', footerDeleteDisabled: Boolean(user && user.inUse), + footerDeleteConfirmMessage: 'Delete this user?', footerDeleteTitle: user && user.inUse ? 'Delete is disabled while this user is the only account left.' : '', footerShowDelete: true, message: message, diff --git a/src/web/views/settings/users/form.hbs b/src/web/views/settings/users/form.hbs index e52cab2..ed3e884 100644 --- a/src/web/views/settings/users/form.hbs +++ b/src/web/views/settings/users/form.hbs @@ -26,7 +26,7 @@ diff --git a/test/users-form-view-model.test.js b/test/users-form-view-model.test.js new file mode 100644 index 0000000..f31e20a --- /dev/null +++ b/test/users-form-view-model.test.js @@ -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); +}); \ No newline at end of file