Add canvas size disable controls
This commit is contained in:
1 parent
68e1f80190
commit
f0601f823d
22 files changed
+205
-87
No files matched your search
@@ -2,6 +2,12 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 2.15.3 - 2026-10-15
|
||||
|
||||
### Changed
|
||||
|
||||
- Replaced the canvas-size duplicate action with reversible disable/enable controls. Disabled sizes remain assigned to existing templates and playlists but cannot be selected for new ones.
|
||||
|
||||
## 2.15.2 - 2026-10-15
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage-player",
|
||||
"version": "2.15.2",
|
||||
"version": "2.15.3",
|
||||
"private": false,
|
||||
"description": "Pulse Signage player application bundle",
|
||||
"engines": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage-web",
|
||||
"version": "2.15.2",
|
||||
"version": "2.15.3",
|
||||
"private": false,
|
||||
"description": "Pulse Signage web and bridge application bundle",
|
||||
"engines": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "2.15.2",
|
||||
"version": "2.15.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pulse-signage",
|
||||
"version": "2.15.2",
|
||||
"version": "2.15.3",
|
||||
"dependencies": {
|
||||
"@sparticuz/chromium": "^149.0.0",
|
||||
"animate.css": "^4.1.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "2.15.2",
|
||||
"version": "2.15.3",
|
||||
"private": false,
|
||||
"description": "Pulse Signage application with MySQL and media storage",
|
||||
"engines": {
|
||||
|
||||
+2
-2
@@ -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`,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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))
|
||||
}));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+57
-20
@@ -7,9 +7,10 @@ const path = require('node:path');
|
||||
require('../src/common');
|
||||
|
||||
const { buildCanvasSizePayload, MAX_CANVAS_SIZE_DIMENSION } = require('../src/data');
|
||||
const { fetchCanvasSizesData } = require('../src/data/canvas-sizes');
|
||||
const { buildCanvasSizeFormViewModel } = require('../src/web/routes/signage/canvas-sizes/form-view-model');
|
||||
const { buildDuplicateCanvasSizeName, buildDuplicateCanvasSize } = require('../src/web/routes/signage/canvas-sizes/duplicate');
|
||||
const registerContentRoutes = require('../src/web/routes/admin/content');
|
||||
const renderCanvasSizesPage = require('../src/web/routes/signage/canvas-sizes/list');
|
||||
const adminPageScript = fs.readFileSync(require.resolve('../src/web/public/js/admin/admin-page.js'), 'utf8');
|
||||
const canvasSizeFormTemplate = fs.readFileSync(require.resolve('../src/web/views/signage/canvas-sizes/form.hbs'), 'utf8');
|
||||
const canvasSizeListTemplate = fs.readFileSync(require.resolve('../src/web/views/signage/canvas-sizes/list.hbs'), 'utf8');
|
||||
@@ -40,23 +41,40 @@ test('canvas size edit form locks dimensions while the size is in use', () => {
|
||||
assert.ok(canvasSizeFormTemplate.includes('data-limit-input-value'));
|
||||
});
|
||||
|
||||
test('buildDuplicateCanvasSize copies canvas size fields for the create page', () => {
|
||||
const duplicate = buildDuplicateCanvasSize({
|
||||
id: 7,
|
||||
name: 'Full HD',
|
||||
width: 1920,
|
||||
height: 1080
|
||||
}, buildDuplicateCanvasSizeName('Full HD'));
|
||||
|
||||
assert.equal(duplicate.id, null);
|
||||
assert.equal(duplicate.name, 'Copy of Full HD');
|
||||
assert.equal(duplicate.width, 1920);
|
||||
assert.equal(duplicate.height, 1080);
|
||||
test('canvas size list replaces duplicate action with status and toggle controls', () => {
|
||||
assert.doesNotMatch(canvasSizeListTemplate, /\/canvas-sizes\/\{\{id\}\}\/duplicate/);
|
||||
assert.match(canvasSizeListTemplate, /data-label="Status"/);
|
||||
assert.match(canvasSizeListTemplate, /\/canvas-sizes\/\{\{id\}\}\/toggle/);
|
||||
assert.match(canvasSizeListTemplate, />\{\{#if enabled\}\}Disable\{\{else\}\}Enable\{\{\/if\}\}<\/button>/);
|
||||
});
|
||||
|
||||
test('canvas size list template includes duplicate action', () => {
|
||||
assert.match(canvasSizeListTemplate, /\/canvas-sizes\/\{\{id\}\}\/duplicate/);
|
||||
assert.match(canvasSizeListTemplate, />Dupe<\/a>/);
|
||||
test('canvas size list renderer preserves enabled state for status labels', () => {
|
||||
const html = renderCanvasSizesPage({
|
||||
canvasSizes: [
|
||||
{ id: 1, name: 'Enabled size', width: 1920, height: 1080, enabled: 1, template_count: 0 },
|
||||
{ id: 2, name: 'Disabled size', width: 1280, height: 720, enabled: 0, template_count: 0 }
|
||||
]
|
||||
}, '', null);
|
||||
|
||||
assert.match(html, /data-label="Status"><span class="badge text-bg-success">Enabled<\/span>/);
|
||||
assert.match(html, /data-label="Status"><span class="badge text-bg-secondary">Disabled<\/span>/);
|
||||
});
|
||||
|
||||
test('canvas-size selectors exclude disabled sizes unless retaining the current assignment', async () => {
|
||||
const queries = [];
|
||||
const pool = {
|
||||
async query(sql, params) {
|
||||
queries.push([sql, params]);
|
||||
return [[]];
|
||||
}
|
||||
};
|
||||
|
||||
await fetchCanvasSizesData(pool);
|
||||
await fetchCanvasSizesData(pool, 9);
|
||||
|
||||
assert.match(queries[0][0], /WHERE enabled = 1/);
|
||||
assert.match(queries[1][0], /WHERE enabled = 1 OR id = \?/);
|
||||
assert.deepEqual(queries[1][1], [9]);
|
||||
});
|
||||
|
||||
test('canvas size update returns a warning when an in-use canvas changes dimensions', async () => {
|
||||
@@ -71,11 +89,19 @@ test('canvas size update returns a warning when an in-use canvas changes dimensi
|
||||
};
|
||||
|
||||
const queries = [];
|
||||
const toggleUpdates = [];
|
||||
const auditEvents = [];
|
||||
let enabled = 1;
|
||||
const pool = {
|
||||
async query(sql) {
|
||||
queries.push(sql);
|
||||
async query(sql, params) {
|
||||
queries.push([sql, params]);
|
||||
if (sql.includes('UPDATE c_canvas_sizes SET enabled')) {
|
||||
enabled = params[0];
|
||||
toggleUpdates.push(params);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.includes('FROM c_canvas_sizes WHERE id = ?')) {
|
||||
return [[{ id: 7, name: 'Full HD', width: 1920, height: 1080 }]];
|
||||
return [[{ id: 7, name: 'Full HD', width: 1920, height: 1080, enabled: enabled }]];
|
||||
}
|
||||
if (sql.includes('SELECT COUNT(*) AS ref_count FROM c_templates WHERE canvas_size_id = ?')) {
|
||||
return [[{ ref_count: 1 }]];
|
||||
@@ -92,7 +118,7 @@ test('canvas size update returns a warning when an in-use canvas changes dimensi
|
||||
getSortQuery: () => '',
|
||||
getSortDirectionQuery: () => 'asc',
|
||||
fetchCanvasSizeById: async (connection, id) => {
|
||||
const [rows] = await connection.query('SELECT id, name, width, height, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes WHERE id = ?', [id]);
|
||||
const [rows] = await connection.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;
|
||||
},
|
||||
buildCanvasSizePayload: buildCanvasSizePayload,
|
||||
@@ -124,6 +150,7 @@ test('canvas size update returns a warning when an in-use canvas changes dimensi
|
||||
removeUnusedUploadFiles: async () => {},
|
||||
syncPlaylistUploadsOnChange: async () => {},
|
||||
getAuditUserId: () => 1,
|
||||
recordRequestAuditEvent: async (_connection, _req, event) => auditEvents.push(event),
|
||||
redirectAfterSave: () => {},
|
||||
notifyPlayerScreens: async () => 0,
|
||||
broadcastDashboardState: async () => {},
|
||||
@@ -172,6 +199,16 @@ test('canvas size update returns a warning when an in-use canvas changes dimensi
|
||||
assert.equal(res.body, 'This canvas size is still used by one or more templates, so its dimensions cannot be changed.');
|
||||
assert.equal(res.redirectedTo, '');
|
||||
assert.equal(queries.length, 2);
|
||||
|
||||
const toggleHandler = handlers['/canvas-sizes/:id/toggle'][1];
|
||||
const toggleReq = { params: { id: '7' } };
|
||||
const toggleRes = { redirectedTo: '', redirect(url) { this.redirectedTo = url; } };
|
||||
await toggleHandler(toggleReq, toggleRes, (error) => { throw error; });
|
||||
assert.equal(toggleRes.redirectedTo, '/canvas-sizes?message=Canvas%20size%20disabled.');
|
||||
await toggleHandler(toggleReq, toggleRes, (error) => { throw error; });
|
||||
|
||||
assert.deepEqual(toggleUpdates.map((params) => params[0]), [0, 1]);
|
||||
assert.deepEqual(auditEvents.map((event) => event.eventType), ['canvas-size.disabled', 'canvas-size.enabled']);
|
||||
});
|
||||
|
||||
test('slide upload cleanup route removes pending media assets', async () => {
|
||||
|
||||
@@ -83,4 +83,20 @@ test('playlist rows use the longest video region duration', () => {
|
||||
assert.equal(model.playlistSlides[0].showVideoDurationButton, true);
|
||||
assert.equal(model.playlistSlides[0].videoDurationSeconds, 27.75);
|
||||
assert.equal(model.playlistSlides[0].durationSeconds, 27.75);
|
||||
});
|
||||
|
||||
test('disabled canvas sizes are hidden for new playlists but retained for existing assignments', () => {
|
||||
const canvasSizes = [
|
||||
{ id: 1, name: 'Full HD', width: 1920, height: 1080, enabled: 1 },
|
||||
{ id: 2, name: 'Legacy', width: 1280, height: 720, enabled: 0 }
|
||||
];
|
||||
const data = { playlistSlides: [], slides: [], canvasSizes: canvasSizes };
|
||||
|
||||
const createModel = buildPlaylistFormViewModel({}, data, '', null, {});
|
||||
assert.deepEqual(createModel.canvasSizes.map((size) => size.id), [1]);
|
||||
|
||||
const editModel = buildPlaylistFormViewModel({ id: 12, canvas_id: 2 }, data, '', null, { isEdit: true });
|
||||
assert.deepEqual(editModel.canvasSizes.map((size) => size.id), [1, 2]);
|
||||
assert.match(editModel.canvasSizes[1].label, /Disabled/);
|
||||
assert.equal(editModel.canvasSizes[1].selected, true);
|
||||
});
|
||||
@@ -38,6 +38,7 @@ function createResponse() {
|
||||
test('playlist create and update redirect to the canonical edit path', async () => {
|
||||
const calls = [];
|
||||
let groupInsertParams = null;
|
||||
let canvasSizeEnabled = 1;
|
||||
const connection = {
|
||||
async beginTransaction() {},
|
||||
async commit() {},
|
||||
@@ -76,7 +77,7 @@ test('playlist create and update redirect to the canonical edit path', async ()
|
||||
},
|
||||
common: {
|
||||
async fetchDuplicateName() { return null; },
|
||||
async fetchCanvasSizeById() { return { id: 7 }; },
|
||||
async fetchCanvasSizeById() { return { id: 7, enabled: canvasSizeEnabled }; },
|
||||
async fetchPlaylistById() { return { id: 4, canvas_id: 7 }; }
|
||||
},
|
||||
pages: {},
|
||||
@@ -161,6 +162,24 @@ test('playlist create and update redirect to the canonical edit path', async ()
|
||||
'insert-slide',
|
||||
{ kind: 'redirectAfterSave', url: '/playlists/4/edit', path: '/playlists/4' }
|
||||
]);
|
||||
|
||||
canvasSizeEnabled = 0;
|
||||
const disabledCreateRes = createResponse();
|
||||
await createHandlers[1]({
|
||||
path: '/playlists',
|
||||
body: { name: 'Disabled size playlist', canvas_size_id: '7', slide_id: [] }
|
||||
}, disabledCreateRes, (error) => { throw error; });
|
||||
assert.equal(disabledCreateRes.statusCode, 400);
|
||||
assert.equal(disabledCreateRes.body, 'Canvas size is disabled.');
|
||||
|
||||
const retainDisabledRes = createResponse();
|
||||
await updateHandlers[1]({
|
||||
path: '/playlists/4',
|
||||
params: { id: '4' },
|
||||
body: { name: 'Lunch Loop', canvas_size_id: '7', slide_id: [] }
|
||||
}, retainDisabledRes, (error) => { throw error; });
|
||||
assert.equal(retainDisabledRes.redirectedTo, '/playlists/4/edit');
|
||||
|
||||
for (const pickCount of ['0', '2']) {
|
||||
const invalidRes = createResponse();
|
||||
await updateHandlers[1]({
|
||||
|
||||
@@ -61,13 +61,22 @@ test('playlist shuffle migration upgrades an existing 2.15.0 installation', asyn
|
||||
test('playlist repeat migration upgrades an existing 2.15.1 installation', async () => {
|
||||
const pool = createPool([]);
|
||||
const pending = await getPendingMigrations(pool, { currentVersion: '2.15.1' });
|
||||
assert.equal(pending.length, 1);
|
||||
assert.equal(pending[0].version, '2.15.2');
|
||||
await pending[0].run(pool);
|
||||
const repeatMigration = pending.find(migration => migration.version === '2.15.2');
|
||||
assert.ok(repeatMigration);
|
||||
await repeatMigration.run(pool);
|
||||
assert.ok(pool.queries.some(([sql]) => sql.includes('ADD COLUMN group_mode')));
|
||||
assert.ok(pool.queries.some(([sql]) => sql.includes('ADD COLUMN group_repeat_count')));
|
||||
});
|
||||
|
||||
test('canvas size enabled migration upgrades an existing 2.15.2 installation', async () => {
|
||||
const pool = createPool([]);
|
||||
const pending = await getPendingMigrations(pool, { currentVersion: '2.15.2' });
|
||||
assert.equal(pending.length, 1);
|
||||
assert.equal(pending[0].version, '2.15.3');
|
||||
await pending[0].run(pool);
|
||||
assert.ok(pool.queries.some(([sql]) => sql.includes('ALTER TABLE c_canvas_sizes ADD COLUMN enabled')));
|
||||
});
|
||||
|
||||
test('playlist group schema migration is pending for version 2.14.3', async () => {
|
||||
const pool = createPool([
|
||||
{
|
||||
|
||||
@@ -124,6 +124,30 @@ test('buildTemplatePayload resolves canvas size and rejects duplicate region nam
|
||||
);
|
||||
});
|
||||
|
||||
test('disabled canvas sizes cannot be assigned to new templates but existing assignments can be retained', async () => {
|
||||
const pool = {
|
||||
async query() {
|
||||
return [[{ id: 4, name: 'Legacy', width: 1280, height: 720, enabled: 0 }]];
|
||||
}
|
||||
};
|
||||
const req = {
|
||||
body: {
|
||||
name: 'Legacy template',
|
||||
canvas_size_id: '4',
|
||||
regions_json: JSON.stringify([{ region_name: 'Body', region_type: 'text' }])
|
||||
},
|
||||
files: []
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => buildTemplatePayload(pool, req, null),
|
||||
(error) => error && error.statusCode === 400 && error.message === 'Canvas size is disabled.'
|
||||
);
|
||||
|
||||
const payload = await buildTemplatePayload(pool, req, { canvas_size_id: 4 });
|
||||
assert.equal(payload.canvasSizeId, 4);
|
||||
});
|
||||
|
||||
test('buildDuplicateTemplate copies template fields and strips row identity', () => {
|
||||
const duplicate = buildDuplicateTemplate({
|
||||
id: 9,
|
||||
|
||||
Reference in new issue
Block a user