Add canvas size disable controls
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m35s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 59s

This commit is contained in:
lzstealth committed 2026-10-15 02:09:34 +01:00
1 parent 68e1f80190
commit f0601f823d
22 files changed
+205 -87

No files matched your search

+2 -2
View File
@@ -4,7 +4,7 @@ const { fetchPagedRows } = require('#src/data/utils');
async function fetchAdminData(pool) {
const [playlists] = await pool.query('SELECT id, name, fade_between_slides, skip_unavailable_rtmp, created_at, modified_at, created_by, modified_by FROM c_playlists ORDER BY id DESC');
const [canvasSizes] = await pool.query('SELECT id, name, width, height, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes ORDER BY width ASC, height ASC, name ASC');
const [canvasSizes] = await pool.query('SELECT id, name, width, height, enabled, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes ORDER BY width ASC, height ASC, name ASC');
const [templates] = await pool.query(`
SELECT st.id, st.name, st.canvas_size_id, st.background_image_path, st.background_color, st.background_gradient, st.created_at, st.modified_at,
cs.name AS canvas_size_name, cs.width AS canvas_size_width, cs.height AS canvas_size_height
@@ -141,7 +141,7 @@ async function fetchTemplatesPage(pool, page, pageSize, searchTerm, sortKey, sor
async function fetchCanvasSizesPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) {
const paged = await fetchPagedRows(pool, {
selectSql: `SELECT id, name, width, height, created_at, modified_at, created_by, modified_by,
selectSql: `SELECT id, name, width, height, enabled, created_at, modified_at, created_by, modified_by,
(SELECT COUNT(*) FROM c_templates st WHERE st.canvas_size_id = c_canvas_sizes.id) AS template_count
FROM c_canvas_sizes
ORDER BY width ASC, height ASC, name ASC`,
+7 -4
View File
@@ -3,14 +3,17 @@
const { fetchPagedRows } = require('#src/data/utils');
const MAX_CANVAS_SIZE_DIMENSION = 16384;
async function fetchCanvasSizesData(pool) {
const [canvasSizes] = await pool.query('SELECT id, name, width, height, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes ORDER BY width ASC, height ASC, name ASC');
async function fetchCanvasSizesData(pool, includeDisabledId) {
const disabledId = Number(includeDisabledId);
const [canvasSizes] = disabledId > 0
? await pool.query('SELECT id, name, width, height, enabled, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes WHERE enabled = 1 OR id = ? ORDER BY width ASC, height ASC, name ASC', [disabledId])
: await pool.query('SELECT id, name, width, height, enabled, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes WHERE enabled = 1 ORDER BY width ASC, height ASC, name ASC');
return { canvasSizes };
}
async function fetchCanvasSizesPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) {
const paged = await fetchPagedRows(pool, {
selectSql: 'SELECT id, name, width, height, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes ORDER BY width ASC, height ASC, name ASC',
selectSql: 'SELECT id, name, width, height, enabled, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes ORDER BY width ASC, height ASC, name ASC',
countSql: 'SELECT COUNT(*) AS count FROM c_canvas_sizes',
searchColumns: ['name', 'width', 'height'],
searchTerm: searchTerm,
@@ -30,7 +33,7 @@ async function fetchCanvasSizesPage(pool, page, pageSize, searchTerm, sortKey, s
}
async function fetchCanvasSizeById(pool, id) {
const [rows] = await pool.query('SELECT id, name, width, height, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes WHERE id = ?', [id]);
const [rows] = await pool.query('SELECT id, name, width, height, enabled, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes WHERE id = ?', [id]);
return rows[0] || null;
}
+6 -1
View File
@@ -264,13 +264,18 @@ async function buildTemplatePayload(pool, req, existingTemplate) {
let resolvedCanvasSizeId = canvasSizeId;
if (resolvedCanvasSizeId) {
const [canvasSizes] = await pool.query('SELECT id, name, width, height, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes WHERE id = ?', [resolvedCanvasSizeId]);
const [canvasSizes] = await pool.query('SELECT id, name, width, height, enabled, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes WHERE id = ?', [resolvedCanvasSizeId]);
const canvasSize = canvasSizes[0];
if (!canvasSize) {
const error = new Error('Canvas size not found.');
error.statusCode = 400;
throw error;
}
if ((canvasSize.enabled === 0 || canvasSize.enabled === false) && (!existingTemplate || Number(existingTemplate.canvas_size_id) !== Number(canvasSize.id))) {
const error = new Error('Canvas size is disabled.');
error.statusCode = 400;
throw error;
}
canvasWidth = Number(canvasSize.width);
canvasHeight = Number(canvasSize.height);
} else {
+1
View File
@@ -29,6 +29,7 @@ async function ensureSchema(pool, options) {
name VARCHAR(255) NOT NULL,
width INT NOT NULL,
height INT NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by INT NULL,
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+7
View File
@@ -677,6 +677,13 @@ const VERSIONED_MIGRATIONS = [
await ensureColumn(pool, 'c_playlist_slides', 'group_mode', "VARCHAR(16) NOT NULL DEFAULT 'select'", 'group_shuffle');
await ensureColumn(pool, 'c_playlist_slides', 'group_repeat_count', 'INT NOT NULL DEFAULT 1', 'group_mode');
}
},
{
version: '2.15.3',
label: 'v2.15.3 canvas size enable state',
run: async function (pool) {
await ensureColumn(pool, 'c_canvas_sizes', 'enabled', 'TINYINT(1) NOT NULL DEFAULT 1', 'height');
}
}
];
+21 -23
View File
@@ -36,7 +36,6 @@ module.exports = function registerContentRoutes(app, deps) {
const hasAnyPermission = deps.hasAnyPermission;
const { buildPagination } = require('#src/web/lib/pagination');
const { buildDuplicateSlideName, buildDuplicateSlide } = require('#src/web/routes/signage/slides/duplicate');
const { buildDuplicateCanvasSizeName, buildDuplicateCanvasSize } = require('#src/web/routes/signage/canvas-sizes/duplicate');
const LIST_PAGE_SIZE = 25;
const WYSIWYG_IMAGE_UPLOAD_MAX_BYTES = 2 * 1024 * 1024;
@@ -337,7 +336,11 @@ module.exports = function registerContentRoutes(app, deps) {
duplicateIndex += 1;
}
res.send(pages.renderTemplateAddPage(buildDuplicateTemplate(template, duplicateName), req.query.message ? String(req.query.message) : 'Review the copied values and save when ready.', canvasSizes.canvasSizes, req.currentUser, null, Number(settings['uploads.image_max_bytes']) || IMAGE_UPLOAD_MAX_BYTES));
const duplicateTemplate = buildDuplicateTemplate(template, duplicateName);
if (!canvasSizes.canvasSizes.some((size) => Number(size.id) === Number(duplicateTemplate.canvas_size_id))) {
duplicateTemplate.canvas_size_id = null;
}
res.send(pages.renderTemplateAddPage(duplicateTemplate, req.query.message ? String(req.query.message) : 'Review the copied values and save when ready.', canvasSizes.canvasSizes, req.currentUser, null, Number(settings['uploads.image_max_bytes']) || IMAGE_UPLOAD_MAX_BYTES));
} catch (error) {
next(error);
}
@@ -749,7 +752,7 @@ module.exports = function registerContentRoutes(app, deps) {
}
template.region_usage = await fetchTemplateRegionUsage(template);
template.inUse = (await fetchSlidesByTemplateId(template.id)).length > 0;
const sizeData = await common.fetchCanvasSizesData(pool);
const sizeData = await common.fetchCanvasSizesData(pool, template.canvas_size_id);
await syncMediaAssetsFromDirectory(pool, deps.uploadDir);
const mediaAssets = await fetchMediaAssets(pool);
const settings = await fetchAppSettings(pool);
@@ -882,26 +885,6 @@ module.exports = function registerContentRoutes(app, deps) {
res.send(pages.renderCanvasSizeAddPage(null, req.query.message ? String(req.query.message) : '', req.currentUser));
});
app.get('/canvas-sizes/:id/duplicate', requirePermission('canvas-sizes.read'), requirePermission('canvas-sizes.create'), async function (req, res, next) {
try {
const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id));
if (!canvasSize) {
return res.status(404).send('Canvas size not found');
}
let duplicateName = buildDuplicateCanvasSizeName(canvasSize.name);
let duplicateIndex = 2;
while (await common.fetchDuplicateName(pool, 'c_canvas_sizes', duplicateName)) {
duplicateName = buildDuplicateCanvasSizeName(canvasSize.name) + ' (' + duplicateIndex + ')';
duplicateIndex += 1;
}
res.send(pages.renderCanvasSizeAddPage(buildDuplicateCanvasSize(canvasSize, duplicateName), req.query.message ? String(req.query.message) : 'Review the copied values and save when ready.', req.currentUser));
} catch (error) {
next(error);
}
});
app.post('/canvas-sizes', requirePermission('canvas-sizes.create'), async function (req, res, next) {
try {
const payload = common.buildCanvasSizePayload(req, null);
@@ -975,6 +958,21 @@ module.exports = function registerContentRoutes(app, deps) {
}
});
app.post('/canvas-sizes/:id/toggle', requirePermission('canvas-sizes.update'), async function (req, res, next) {
try {
const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id));
if (!canvasSize) {
return res.status(404).send('Canvas size not found');
}
const enabled = canvasSize.enabled === 0 || canvasSize.enabled === false ? 1 : 0;
await pool.query('UPDATE c_canvas_sizes SET enabled = ?, modified_by = ? WHERE id = ?', [enabled, getAuditUserId(req), canvasSize.id]);
if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'canvas-sizes', eventType: enabled ? 'canvas-size.enabled' : 'canvas-size.disabled', actorUserId: getAuditUserId(req), targetType: 'canvas-size', targetId: canvasSize.id, targetLabel: canvasSize.name });
res.redirect('/canvas-sizes?message=' + encodeURIComponent(enabled ? 'Canvas size enabled.' : 'Canvas size disabled.'));
} catch (error) {
next(error);
}
});
app.post('/canvas-sizes/:id/delete', requirePermission('canvas-sizes.delete'), async function (req, res, next) {
try {
const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id));
+6
View File
@@ -134,6 +134,9 @@ module.exports = function registerPlaylistRoutes(app, deps) {
throw createValidationError('Canvas size not found.');
}
requestedCanvasId = Number(canvasSize.id);
if ((canvasSize.enabled === 0 || canvasSize.enabled === false) && (!shouldUpdatePlaylist || Number(playlist.canvas_id) !== requestedCanvasId)) {
throw createValidationError('Canvas size is disabled.');
}
if (Number.isInteger(Number(playlist.canvas_id)) && Number(playlist.canvas_id) > 0 && Number(playlist.canvas_id) !== requestedCanvasId) {
throw new Error('Canvas size cannot be changed after the playlist is created.');
}
@@ -387,6 +390,9 @@ module.exports = function registerPlaylistRoutes(app, deps) {
if (!canvasSize) {
return res.status(400).send('Canvas size not found.');
}
if (canvasSize.enabled === 0 || canvasSize.enabled === false) {
return res.status(400).send('Canvas size is disabled.');
}
const actorId = getAuditUserId(req);
await connection.beginTransaction();
const [result] = await connection.query('INSERT INTO c_playlists (name, fade_between_slides, skip_unavailable_rtmp, canvas_id, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?)', [name, fadeBetweenSlides, skipUnavailableRtmp, canvasSize.id, actorId, actorId]);
@@ -1,21 +0,0 @@
// Canvas size duplicate helpers.
function buildDuplicateCanvasSizeName(name) {
const source = String(name || 'Canvas size').trim() || 'Canvas size';
return 'Copy of ' + source;
}
function buildDuplicateCanvasSize(canvasSize, duplicateName) {
const source = canvasSize || {};
return {
id: null,
name: String(duplicateName || buildDuplicateCanvasSizeName(source.name)).trim(),
width: Math.max(1, Number(source.width || 1920)),
height: Math.max(1, Number(source.height || 1080))
};
}
module.exports = {
buildDuplicateCanvasSizeName,
buildDuplicateCanvasSize
};
@@ -8,6 +8,7 @@ module.exports = function renderCanvasSizesPage(data, message, currentUser) {
name: size.name,
width: size.width,
height: size.height,
enabled: size.enabled !== 0 && size.enabled !== false,
templateCount: size.template_count !== undefined
? Number(size.template_count) || 0
: (data.templates || []).filter((template) => Number(template.canvas_size_id) === Number(size.id)).length
@@ -228,8 +228,11 @@ function buildPlaylistFormViewModel(playlist, data, message, currentUser, option
const computedPlaylistCanvasId = playlistSlideCanvasIds.length === 1 ? playlistSlideCanvasIds[0] : null;
const playlistCanvasId = storedCanvasId || computedPlaylistCanvasId;
const selectedCanvasSize = (data.canvasSizes || []).find((size) => Number(size.id) === Number(playlistCanvasId)) || null;
const canvasSizes = (data.canvasSizes || []).map((size) => Object.assign({}, size, {
label: formatCanvasSizeLabel(size),
const canvasSizes = (data.canvasSizes || []).filter((size) => {
const enabled = size.enabled !== 0 && size.enabled !== false;
return enabled || (playlistCanvasId && Number(size.id) === playlistCanvasId);
}).map((size) => Object.assign({}, size, {
label: formatCanvasSizeLabel(size) + (size.enabled === 0 || size.enabled === false ? ' (Disabled)' : ''),
canvasId: Number(size.id) || null,
selected: Boolean(selectedCanvasSize && Number(selectedCanvasSize.id) === Number(size.id))
}));
+8 -4
View File
@@ -24,6 +24,7 @@
<tr>
<th data-table-sort-key="name">Name</th>
<th data-table-sort-key="dimensions">Dimensions</th>
<th>Status</th>
<th data-table-sort-key="templates">Templates</th>
<th>Actions</th>
</tr>
@@ -34,15 +35,18 @@
<tr data-table-search-row>
<td data-label="Name">{{name}}</td>
<td data-label="Dimensions">{{width}}x{{height}}</td>
<td data-label="Status"><span class="badge text-bg-{{#if enabled}}success{{else}}secondary{{/if}}">{{#if enabled}}Enabled{{else}}Disabled{{/if}}</span></td>
<td data-label="Templates"><span class="chip">{{templateCount}} template{{#if (gt templateCount 1)}}s{{else}}{{/if}}{{#if (lt templateCount 1)}}s{{/if}}</span></td>
<td data-label="Actions">
{{#if (anyPermission ../currentUser 'canvas-sizes.create' 'canvas-sizes.update' 'canvas-sizes.delete')}}
{{#if (anyPermission ../currentUser 'canvas-sizes.update' 'canvas-sizes.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'canvas-sizes.update')}}
<a class="btn btn-sm btn-primary" href="/canvas-sizes/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'canvas-sizes.create')}}
<a class="btn btn-sm btn-secondary" href="/canvas-sizes/{{id}}/duplicate">Dupe</a>
{{#if (hasPermission ../currentUser 'canvas-sizes.update')}}
<form class="inline-form" method="post" action="/canvas-sizes/{{id}}/toggle" data-confirm-message="{{#if enabled}}Disable{{else}}Enable{{/if}} this canvas size?">
<button class="btn btn-sm btn-secondary" type="submit">{{#if enabled}}Disable{{else}}Enable{{/if}}</button>
</form>
{{/if}}
{{#if (hasPermission ../currentUser 'canvas-sizes.delete')}}
<form class="inline-form" method="post" action="/canvas-sizes/{{id}}/delete" data-confirm-message="Delete this canvas size?">
@@ -61,7 +65,7 @@
</tr>
{{/each}}
{{else}}
<tr data-table-search-empty-default><td colspan="4" class="empty">No canvas sizes yet.</td></tr>
<tr data-table-search-empty-default><td colspan="5" class="empty">No canvas sizes yet.</td></tr>
{{/if}}
</tbody>
</table>
+1 -1
View File
@@ -82,7 +82,7 @@
{{/unless}}
{{#if canvasSizes.length}}
{{#each canvasSizes}}
<option value="{{id}}" data-width="{{width}}" data-height="{{height}}" {{#if (eq ../template.canvas_size_id id)}}selected{{/if}}>{{name}} ({{width}}x{{height}})</option>
<option value="{{id}}" data-width="{{width}}" data-height="{{height}}" {{#if (eq ../template.canvas_size_id id)}}selected{{/if}}>{{name}} ({{width}}x{{height}}){{#unless enabled}} (Disabled){{/unless}}</option>
{{/each}}
{{/if}}
</select>