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
@@ -0,0 +1,20 @@
// Announcement duplication helpers.
function buildDuplicateAnnouncementName(announcementName) {
const baseName = 'Copy of ' + String(announcementName || '').trim();
return baseName;
}
function buildDuplicateAnnouncement(announcement, duplicateName) {
return Object.assign({}, announcement, {
id: null,
short_label: duplicateName,
expires_at: null,
screen_ids: Array.isArray(announcement && announcement.screen_ids) ? announcement.screen_ids.slice() : []
});
}
module.exports = {
buildDuplicateAnnouncementName,
buildDuplicateAnnouncement
};
@@ -24,8 +24,11 @@ function buildAnnouncementFormViewModel(announcement, message, currentUser, opti
const announcementColors = Array.isArray(options && options.announcementColors) ? options.announcementColors : [];
const announcementIcons = Array.isArray(options && options.announcementIcons) ? options.announcementIcons : [];
const announcementScreens = Array.isArray(options && options.announcementScreens) ? options.announcementScreens : [];
const viewOptions = options || {};
const viewAnnouncement = Object.assign({}, buildDefaultAnnouncement(), announcement || {});
const canDelete = Boolean(currentUser && Array.isArray(currentUser.permissionKeys) && currentUser.permissionKeys.indexOf('announcements.delete') !== -1);
const hasTargets = Array.isArray(viewAnnouncement.screen_ids) && viewAnnouncement.screen_ids.length > 0;
const isActive = !(viewAnnouncement.expires_at && new Date(viewAnnouncement.expires_at).getTime() <= Date.now());
return {
title: isEdit ? 'Edit announcement' : 'Add announcement',
@@ -34,9 +37,29 @@ function buildAnnouncementFormViewModel(announcement, message, currentUser, opti
currentUser: currentUser || null,
announcement: viewAnnouncement,
isEdit: Boolean(isEdit),
showSaveSecondaryActions: Boolean(isEdit),
showDeleteAction: Boolean(isEdit && canDelete),
showSaveSecondaryActions: true,
messageVariant: viewOptions.messageVariant || '',
actionLabel: isActive ? 'Stop' : 'Play',
actionIcon: isActive ? 'bi-stop-fill' : 'bi-play-fill',
actionDisabled: !isActive && !hasTargets,
actionClassName: !isActive && !hasTargets
? 'btn-outline-info'
: (isActive ? 'btn-outline-warning' : 'btn-info'),
actionDisabledTitle: !isActive && !hasTargets
? 'Select at least one screen group to play this announcement.'
: '',
actionConfirmMessage: isActive
? 'Stop this announcement on the selected screens now?'
: 'Send this announcement to the selected screens now?',
actionPath: isEdit && viewAnnouncement.id
? (isActive ? '/announcements/' + viewAnnouncement.id + '/stop' : '/announcements/' + viewAnnouncement.id + '/play')
: '',
showDeleteAction: Boolean(canDelete || !isEdit),
deleteDisabled: !isEdit || !Boolean(viewAnnouncement && viewAnnouncement.canDelete),
deleteConfirmMessage: isEdit ? 'Delete this announcement?' : '',
deleteTitle: !isEdit
? 'Delete is available after the announcement has been created.'
: (!Boolean(viewAnnouncement && viewAnnouncement.canDelete) ? 'This announcement is currently active and cannot be deleted.' : ''),
formAction: isEdit && viewAnnouncement && viewAnnouncement.id ? '/announcements/' + viewAnnouncement.id : '/announcements',
formAttrs: 'data-async-save' + (isEdit ? ' data-async-save-close-url="/announcements"' : ' data-async-save-new-redirect="response-url"') + ' data-async-save-new-url="/announcements/new"',
cancelUrl: '/announcements',
+15 -1
View File
@@ -21,10 +21,15 @@ function formatStatusLabel(announcement) {
function formatActionButton(announcement) {
const isActive = formatStatusLabel(announcement) === 'Active';
const hasTargets = Math.max(0, Number(announcement && announcement.screen_target_count) || 0) > 0;
return {
label: isActive ? 'Stop' : 'Play',
icon: isActive ? 'bi-stop-fill' : 'bi-play-fill',
className: isActive ? 'btn-outline-warning' : 'btn-outline-success',
disabled: !isActive && !hasTargets,
className: !isActive && !hasTargets
? 'btn-outline-info'
: (isActive ? 'btn-outline-warning' : 'btn-info'),
disabledTitle: !isActive && !hasTargets ? 'Select at least one screen group to play this announcement.' : '',
confirmMessage: isActive
? 'Stop this announcement on the selected screens now?'
: 'Send this announcement to the selected screens now?',
@@ -32,6 +37,10 @@ function formatActionButton(announcement) {
};
}
function isAnnouncementDeletionBlocked(announcement) {
return formatStatusLabel(announcement) === 'Active';
}
function formatIconLabel(iconKey) {
const normalized = String(iconKey || '').trim().toLowerCase() || DEFAULT_ANNOUNCEMENT_ICON;
return ANNOUNCEMENT_ICON_LABELS[normalized] || normalized;
@@ -69,6 +78,7 @@ function formatScreenTargetsLabel(announcement) {
module.exports = function renderAnnouncementsPage(data, message, currentUser) {
const announcements = (data.announcements || []).map(function (announcement) {
const actionButton = formatActionButton(announcement);
const deleteDisabled = isAnnouncementDeletionBlocked(announcement);
return Object.assign({}, announcement, {
shortLabel: String(announcement.short_label || '').trim(),
iconKey: String(announcement.icon_key || DEFAULT_ANNOUNCEMENT_ICON).trim().toLowerCase() || DEFAULT_ANNOUNCEMENT_ICON,
@@ -77,9 +87,13 @@ module.exports = function renderAnnouncementsPage(data, message, currentUser) {
expiresAtLabel: formatExpiresLabel(announcement.expires_at),
screenTargetsLabel: formatScreenTargetsLabel(announcement),
statusLabel: formatStatusLabel(announcement),
deleteDisabled: deleteDisabled,
deleteBlockedMessage: deleteDisabled ? 'This announcement is currently active and cannot be deleted.' : '',
actionButtonLabel: actionButton.label,
actionButtonIcon: actionButton.icon,
actionButtonClassName: actionButton.className,
actionButtonDisabled: actionButton.disabled,
actionButtonDisabledTitle: actionButton.disabledTitle,
actionButtonConfirmMessage: actionButton.confirmMessage,
actionButtonPath: actionButton.actionPath
});
+43 -8
View File
@@ -11,12 +11,13 @@ module.exports = function registerAnnouncementRoutes(app, deps) {
const { buildPagination } = require('../../../lib/pagination');
const announcementData = require('#src/data/announcements');
const announcementIcons = require('#src/data/announcement-icons');
const { buildDuplicateAnnouncementName, buildDuplicateAnnouncement } = require('./duplicate');
const LIST_PAGE_SIZE = 25;
const ANNOUNCEMENT_TYPES = [
{ value: 'lower-third', label: 'Lower third' },
{ value: 'fullscreen', label: 'Fullscreen' },
{ value: 'top-banner', label: 'Top banner' }
{ value: 'lower-third', label: 'Lower third', icon: 'arrow-bar-down' },
{ value: 'fullscreen', label: 'Fullscreen', icon: 'arrows-fullscreen' },
{ value: 'top-banner', label: 'Top banner', icon: 'arrow-bar-up' }
];
const ANNOUNCEMENT_COLORS = [
{ value: 'primary', label: 'Primary' },
@@ -63,8 +64,7 @@ module.exports = function registerAnnouncementRoutes(app, deps) {
}
function isAnnouncementDeletionBlocked(announcement) {
const screenIds = Array.isArray(announcement && announcement.screen_ids) ? announcement.screen_ids : [];
return isAnnouncementActive(announcement) || screenIds.length > 0;
return isAnnouncementActive(announcement);
}
async function fetchAllScreenSlugs() {
@@ -221,6 +221,10 @@ module.exports = function registerAnnouncementRoutes(app, deps) {
const durationMode = String(normalized.durationMode || '').trim() || (durationSeconds > 0 ? 'duration' : 'until_disabled');
const isActive = isAnnouncementActive(normalized);
const canDelete = Boolean(normalized.id) && !isAnnouncementDeletionBlocked(normalized);
const hasTargets = Array.isArray(normalized.screen_ids) && normalized.screen_ids.length > 0;
const deleteBlockedMessage = canDelete
? ''
: 'This announcement is currently active and cannot be deleted.';
return Object.assign({}, normalized, {
durationMode: durationMode,
@@ -230,7 +234,11 @@ module.exports = function registerAnnouncementRoutes(app, deps) {
isActive: isActive,
actionLabel: isActive ? 'Stop' : 'Play',
actionIcon: isActive ? 'bi-stop-fill' : 'bi-play-fill',
actionClassName: isActive ? 'btn-outline-warning' : 'btn-outline-success',
actionDisabled: !isActive && !hasTargets,
actionClassName: !isActive && !hasTargets
? 'btn-outline-info'
: (isActive ? 'btn-outline-warning' : 'btn-info'),
actionDisabledTitle: !isActive && !hasTargets ? 'Select at least one screen group to play this announcement.' : '',
actionConfirmMessage: isActive
? 'Stop this announcement on the selected screens now?'
: 'Send this announcement to the selected screens now?',
@@ -238,7 +246,7 @@ module.exports = function registerAnnouncementRoutes(app, deps) {
? `/announcements/${normalized.id}/stop`
: `/announcements/${normalized.id}/play`,
canDelete: canDelete,
deleteBlockedMessage: canDelete ? '' : 'This announcement is still in use by one or more screens.'
deleteBlockedMessage: deleteBlockedMessage
});
}
@@ -301,6 +309,33 @@ module.exports = function registerAnnouncementRoutes(app, deps) {
}
});
app.get('/announcements/:id/duplicate', requirePermission('announcements.read'), requirePermission('announcements.create'), async function (req, res, next) {
try {
const announcement = await common.fetchAnnouncementById(pool, Number(req.params.id));
if (!announcement) {
return res.status(404).send('Announcement not found');
}
const screens = await fetchAnnouncementScreens();
let duplicateName = buildDuplicateAnnouncementName(announcement.short_label || announcement.message || 'Announcement');
let duplicateIndex = 2;
while (await common.fetchDuplicateName(pool, 'd_announcements', duplicateName, null, 'short_label')) {
duplicateName = buildDuplicateAnnouncementName(announcement.short_label || announcement.message || 'Announcement') + ' (' + duplicateIndex + ')';
duplicateIndex += 1;
}
res.send(pages.renderAnnouncementAddPage(buildAnnouncementFormData(buildDuplicateAnnouncement(announcement, duplicateName)), req.query.message ? String(req.query.message) : 'Review the copied values and save when ready.', req.currentUser, {
announcementTypes: ANNOUNCEMENT_TYPES,
announcementColors: ANNOUNCEMENT_COLORS,
announcementIcons: ANNOUNCEMENT_ICONS,
announcementScreens: buildAnnouncementScreenOptions(screens, announcement.screen_ids || []),
messageVariant: 'info'
}));
} catch (error) {
next(error);
}
});
app.post('/announcements', requirePermission('announcements.create'), async function (req, res, next) {
try {
const message = String(req.body.message || '').replace(/\r\n/g, '\n').trim();
@@ -421,7 +456,7 @@ module.exports = function registerAnnouncementRoutes(app, deps) {
}
if (isAnnouncementDeletionBlocked(announcement)) {
return res.redirect('/announcements/' + announcement.id + '/edit?message=' + encodeURIComponent('This announcement is still in use by one or more screens.'));
return res.redirect('/announcements/' + announcement.id + '/edit?message=' + encodeURIComponent('This announcement is currently active and cannot be deleted.'));
}
await pool.query('DELETE FROM d_announcements WHERE id = ?', [announcement.id]);