From 9d5029eff695eeb2812ac3da7b0f72b6908fae80 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Mon, 3 Aug 2026 19:18:30 +0100 Subject: [PATCH] Release 2.5.2 --- .gitignore | 1 + CHANGELOG.md | 12 +++++ package.json | 2 +- src/db/bootstrap.js | 2 +- src/db/migrations.js | 54 ++++++++++++++----- .../lower-third.template.html | 5 +- .../top-banner.template.html | 5 +- src/player/player-page-announcements.js | 7 +-- src/player/public/css/player.css | 5 +- src/player/public/js/player-page-commands.js | 7 +-- src/player/render.js | 2 +- src/player/runtime.js | 33 +++++++++++- src/request-auth.js | 23 +++++++- src/web/public/css/theme-custom.css | 6 ++- src/web/public/js/regions/type/api.js | 3 +- src/web/public/js/regions/type/rss.js | 9 ++-- src/web/public/js/regions/type/rtmp.js | 2 +- src/web/public/js/slides/slide-form-editor.js | 3 +- .../public/js/slides/slide-form-regions.js | 25 +++++++-- src/web/views/signage/templates/form.hbs | 2 +- test/player-runtime.test.js | 36 +++++++++++++ 21 files changed, 195 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index b18d49c..6b09581 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ media/ !src/web/lib/media/ !src/web/lib/media/** docker-compose.dev.yml +/dev-demo-seed.js .vscode/ .env npm-debug.log* diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b2a994..2518733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## 2.5.2 - 2026-08-03 + +### Changed + +- Player page auth now writes a cookie for websocket reuse, and player and announcement sockets accept that cookie so they no longer depend on query-string tokens. +- RSS and API region placeholder panels now use shared field-aware chip rendering, with updated spacing and defaults across the slide editor. +- RTMP regions now default audio to enabled in the editor, and the slide rich-text editor no longer allows anchor tags. + +### Fixed + +- Player websocket auth now falls back to the `pulse_page_auth` cookie when the auth query parameter is unavailable. + ## 2.5.1 - 2026-08-03 ### Added diff --git a/package.json b/package.json index ef8d50b..e7e52f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.5.1", + "version": "2.5.2", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/db/bootstrap.js b/src/db/bootstrap.js index 08b1ccc..428bd8d 100644 --- a/src/db/bootstrap.js +++ b/src/db/bootstrap.js @@ -5,7 +5,7 @@ async function bootstrapDatabase(pool) { const [canvasSizeCountRows] = await pool.query('SELECT COUNT(*) AS canvas_size_count FROM c_canvas_sizes'); if (!canvasSizeCountRows.length || Number(canvasSizeCountRows[0].canvas_size_count) === 0) { await pool.query(` - INSERT INTO c_canvas_sizes (name, width, height) VALUES + INSERT IGNORE INTO c_canvas_sizes (name, width, height) VALUES ('Full HD', 1920, 1080), ('HD', 1280, 720), ('4K UHD', 3840, 2160), diff --git a/src/db/migrations.js b/src/db/migrations.js index 8f21bac..0acf094 100644 --- a/src/db/migrations.js +++ b/src/db/migrations.js @@ -55,16 +55,7 @@ const VERSIONED_MIGRATIONS = [ } // Recreate the screen-to-player foreign key after the column exists and legacy data is copied over. - const [screenPlayerFkRows] = await pool.query( - `SELECT COUNT(*) AS fk_count - FROM information_schema.KEY_COLUMN_USAGE - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'd_screens' - AND CONSTRAINT_NAME = 'fk_screens_player'` - ); - if (Number(screenPlayerFkRows && screenPlayerFkRows[0] && screenPlayerFkRows[0].fk_count) === 0) { - await pool.query('ALTER TABLE d_screens ADD CONSTRAINT fk_screens_player FOREIGN KEY (player_id) REFERENCES d_players(device_id) ON DELETE RESTRICT'); - } + await ensureForeignKey(pool, 'd_screens', 'fk_screens_player', 'player_id', 'd_players', 'device_id', 'RESTRICT'); if (await columnExists(pool, 'c_template_regions', 'font_family')) { await pool.query('ALTER TABLE c_template_regions DROP COLUMN font_family'); @@ -243,24 +234,59 @@ async function ensureColumn(pool, tableName, columnName, columnDefinition, after } const afterClause = afterColumn ? ' AFTER ' + afterColumn : ''; - await pool.query('ALTER TABLE ' + tableName + ' ADD COLUMN ' + columnName + ' ' + columnDefinition + afterClause); + try { + await pool.query('ALTER TABLE ' + tableName + ' ADD COLUMN ' + columnName + ' ' + columnDefinition + afterClause); + } catch (error) { + if (!error || (error.code !== 'ER_DUP_FIELDNAME' && error.errno !== 1060)) { + throw error; + } + } } async function ensureForeignKey(pool, tableName, constraintName, columnName, referencedTable, referencedColumn, onDeleteAction) { - const [rows] = await pool.query( + const [existingFkRows] = await pool.query( `SELECT COUNT(*) AS fk_count FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? + AND COLUMN_NAME = ? + AND REFERENCED_TABLE_NAME IS NOT NULL`, + [tableName, columnName] + ); + + if (Number(existingFkRows && existingFkRows[0] && existingFkRows[0].fk_count) > 0) { + return; + } + + const [rows] = await pool.query( + `SELECT COUNT(*) AS fk_count + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = DATABASE() AND CONSTRAINT_NAME = ?`, - [tableName, constraintName] + [constraintName] ); if (Number(rows && rows[0] && rows[0].fk_count) > 0) { return; } - await pool.query('ALTER TABLE ' + tableName + ' ADD CONSTRAINT ' + constraintName + ' FOREIGN KEY (' + columnName + ') REFERENCES ' + referencedTable + '(' + referencedColumn + ') ON DELETE ' + onDeleteAction); + const fallbackNames = [ + constraintName, + tableName + '_' + columnName + '_fk', + tableName + '_' + columnName + '_fk_2', + tableName + '_' + columnName + '_fk_3' + ]; + + for (const candidateName of fallbackNames) { + try { + await pool.query('ALTER TABLE ' + tableName + ' ADD CONSTRAINT ' + candidateName + ' FOREIGN KEY (' + columnName + ') REFERENCES ' + referencedTable + '(' + referencedColumn + ') ON DELETE ' + onDeleteAction); + return; + } catch (error) { + if (!error || (error.code !== 'ER_FK_DUP_NAME' && error.errno !== 1826)) { + throw error; + } + } + } } async function dropColumnIfExists(pool, tableName, columnName) { diff --git a/src/player/announcement-templates/lower-third.template.html b/src/player/announcement-templates/lower-third.template.html index febcaa3..a47e5e9 100644 --- a/src/player/announcement-templates/lower-third.template.html +++ b/src/player/announcement-templates/lower-third.template.html @@ -101,6 +101,7 @@ gap: 2.5rem; min-width: 100%; width: max-content; + backface-visibility: hidden; will-change: transform; animation: lower-third-scroll var(--announcement-scroll-duration, 20s) linear infinite; } @@ -144,10 +145,10 @@ @keyframes lower-third-scroll { from { - transform: translateX(0); + transform: translate3d(0, 0, 0); } to { - transform: translateX(-50%); + transform: translate3d(-50%, 0, 0); } } diff --git a/src/player/announcement-templates/top-banner.template.html b/src/player/announcement-templates/top-banner.template.html index 4b56a92..dbe6447 100644 --- a/src/player/announcement-templates/top-banner.template.html +++ b/src/player/announcement-templates/top-banner.template.html @@ -103,6 +103,7 @@ gap: 2.5rem; min-width: 100%; width: max-content; + backface-visibility: hidden; will-change: transform; animation: top-banner-scroll var(--announcement-scroll-duration, 20s) linear infinite; } @@ -146,10 +147,10 @@ @keyframes top-banner-scroll { from { - transform: translateX(0); + transform: translate3d(0, 0, 0); } to { - transform: translateX(-50%); + transform: translate3d(-50%, 0, 0); } } diff --git a/src/player/player-page-announcements.js b/src/player/player-page-announcements.js index e2d5b01..3d065ff 100644 --- a/src/player/player-page-announcements.js +++ b/src/player/player-page-announcements.js @@ -215,12 +215,7 @@ return; } - var socketUrl = new URL(announcementSocketPath, window.location.origin); - if (window.__pulsePageAuthToken) { - socketUrl.searchParams.set('auth', window.__pulsePageAuthToken); - } - - var socket = new WebSocket(socketUrl.toString()); + var socket = new WebSocket(new URL(announcementSocketPath, window.location.origin).toString()); announcementSocket = socket; socket.onopen = function () { diff --git a/src/player/public/css/player.css b/src/player/public/css/player.css index bef4abb..68c4034 100644 --- a/src/player/public/css/player.css +++ b/src/player/public/css/player.css @@ -214,6 +214,8 @@ body.thumbnail-preview .player-offline-banner { height: 100%; opacity: 0; transition: opacity 560ms ease; + will-change: opacity; + backface-visibility: hidden; } .slide-shell.is-visible { @@ -243,7 +245,8 @@ body.screen-blackout #app { height: var(--player-canvas-height, 100vh); max-width: 100vw; max-height: 100vh; - transform: translate(-50%, -50%); + transform: translate3d(-50%, -50%, 0); + backface-visibility: hidden; z-index: 9999; pointer-events: none; display: flex; diff --git a/src/player/public/js/player-page-commands.js b/src/player/public/js/player-page-commands.js index d6cdbff..ca6001a 100644 --- a/src/player/public/js/player-page-commands.js +++ b/src/player/public/js/player-page-commands.js @@ -526,12 +526,7 @@ function connectCommandSocket() { if (commandSocket && (commandSocket.readyState === WebSocket.OPEN || commandSocket.readyState === WebSocket.CONNECTING)) { return; } - var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - var socketUrl = new URL(commandSocketPath, window.location.origin); - if (window.__pulsePageAuthToken) { - socketUrl.searchParams.set('auth', window.__pulsePageAuthToken); - } - var socket = new WebSocket(socketUrl.toString()); + var socket = new WebSocket(new URL(commandSocketPath, window.location.origin).toString()); commandSocket = socket; socket.onopen = function () { diff --git a/src/player/render.js b/src/player/render.js index 80477e6..f15807b 100644 --- a/src/player/render.js +++ b/src/player/render.js @@ -132,7 +132,7 @@ function renderPlayerPage(slug, initialData) { bodyClass: bodyClass, body: '
Loading screen...
', stylesheets: fontStylesheetHref ? [fontStylesheetHref] : [], - script: createPageFetchAuthScript(pageAuthToken) + hlsScriptTag + serviceWorkerScript + createThumbnailPreviewBootstrapScript(initialData) + '' + '' + '' + '' + '' + '' + '' + '' + onboardingScript + script + '' + script: createPageFetchAuthScript(pageAuthToken, '/ws/screens/' + encodeURIComponent(slug || '')) + hlsScriptTag + serviceWorkerScript + createThumbnailPreviewBootstrapScript(initialData) + '' + '' + '' + '' + '' + '' + '' + '' + onboardingScript + script + '' }); } diff --git a/src/player/runtime.js b/src/player/runtime.js index 713bfd2..aa7f225 100644 --- a/src/player/runtime.js +++ b/src/player/runtime.js @@ -4,6 +4,7 @@ const crypto = require('crypto'); const { WebSocketServer, WebSocket } = require('ws'); const { isClientNameAvailable } = require('#src/data/client-name-check'); const { verifyPageAuthToken, verifyRequestAuth } = require('#src/request-auth'); +const PAGE_AUTH_COOKIE_NAME = 'pulse_page_auth'; function normalizePlayerPublicBaseUrl(pageUrl) { const value = String(pageUrl || '').trim(); @@ -43,6 +44,34 @@ function createPlayerRuntime(options) { return ip; } + function parseCookies(cookieHeader) { + return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) { + if (!pair) { + return cookies; + } + const separatorIndex = pair.indexOf('='); + if (separatorIndex === -1) { + return cookies; + } + const name = decodeURIComponent(pair.slice(0, separatorIndex).trim()); + const value = decodeURIComponent(pair.slice(separatorIndex + 1).trim()); + if (name) { + cookies[name] = value; + } + return cookies; + }, {}); + } + + function readPageAuthToken(request) { + const queryToken = String(new URL(request.url, 'http://localhost').searchParams.get('auth') || '').trim(); + if (queryToken) { + return queryToken; + } + + const cookies = parseCookies(request.headers && request.headers.cookie || ''); + return String(cookies[PAGE_AUTH_COOKIE_NAME] || '').trim(); + } + function getConnectionBucket(slug) { const key = String(slug || '').trim(); if (!key) { @@ -314,7 +343,7 @@ function createPlayerRuntime(options) { } if (playerMatch) { - const authToken = String(new URL(request.url, 'http://localhost').searchParams.get('auth') || '').trim(); + const authToken = readPageAuthToken(request); const payload = verifyPageAuthToken(authToken); if (!payload || String(payload.scope || '').trim() !== 'player') { socket.destroy(); @@ -323,7 +352,7 @@ function createPlayerRuntime(options) { } if (announcementMatch) { - const authToken = String(new URL(request.url, 'http://localhost').searchParams.get('auth') || '').trim(); + const authToken = readPageAuthToken(request); const payload = verifyPageAuthToken(authToken); if (!payload || String(payload.scope || '').trim() !== 'player') { socket.destroy(); diff --git a/src/request-auth.js b/src/request-auth.js index 62c5bf3..e53a573 100644 --- a/src/request-auth.js +++ b/src/request-auth.js @@ -246,12 +246,13 @@ function verifyRequestAuth(req) { return timingSafeEqualHex(expectedSignature, signature); } -function createPageFetchAuthScript(token) { +function createPageFetchAuthScript(token, cookiePath) { const normalizedToken = String(token && typeof token === 'object' ? token.token : token || '').trim(); if (!normalizedToken) { return ''; } + const normalizedCookiePath = String(cookiePath || '').trim(); const pageAuthExpiresAt = token && typeof token === 'object' && Number.isFinite(Number(token.expiresAt)) ? Number(token.expiresAt) : null; @@ -262,6 +263,8 @@ function createPageFetchAuthScript(token) { ' (function () {', ' var pageAuthToken = ' + JSON.stringify(normalizedToken) + ';', ' var pageAuthExpiresAt = ' + JSON.stringify(pageAuthExpiresAt) + ';', + ' var pageAuthCookiePath = ' + JSON.stringify(normalizedCookiePath) + ';', + ' var pageAuthCookieName = "pulse_page_auth";', ' var pageAuthRenewalTimer = null;', ' var pageAuthRenewalInFlight = null;', ' var pageAuthRenewalSkewMs = ' + JSON.stringify(renewSkewMs) + ';', @@ -287,11 +290,28 @@ function createPageFetchAuthScript(token) { ' });', ' }, delayMs);', ' }', + ' function writePageAuthCookie(nextToken, nextExpiresAt) {', + ' if (!pageAuthCookiePath) {', + ' return;', + ' }', + ' var cookieParts = [pageAuthCookieName + "=" + encodeURIComponent(String(nextToken || "").trim()), "Path=" + pageAuthCookiePath, "SameSite=Lax"];', + ' var expiresInMs = Number(nextExpiresAt || 0) - Date.now();', + ' if (Number.isFinite(expiresInMs) && expiresInMs > 0) {', + ' cookieParts.push("Max-Age=" + Math.max(1, Math.floor(expiresInMs / 1000)));', + ' } else {', + ' cookieParts.push("Max-Age=0");', + ' }', + ' if (window.location.protocol === "https:") {', + ' cookieParts.push("Secure");', + ' }', + ' document.cookie = cookieParts.join("; ");', + ' }', ' function setPageAuthToken(nextToken, nextExpiresAt) {', ' pageAuthToken = String(nextToken || "").trim();', ' pageAuthExpiresAt = Number(nextExpiresAt || 0) || null;', ' window.__pulsePageAuthToken = pageAuthToken;', ' window.__pulsePageAuthExpiresAt = pageAuthExpiresAt;', + ' writePageAuthCookie(pageAuthToken, pageAuthExpiresAt);', ' schedulePageAuthRenewal();', ' }', ' async function renewPageAuthToken() {', @@ -326,6 +346,7 @@ function createPageFetchAuthScript(token) { ' window.__pulseRenewPageAuthToken = renewPageAuthToken;', ' window.__pulsePageAuthToken = pageAuthToken;', ' window.__pulsePageAuthExpiresAt = pageAuthExpiresAt;', + ' writePageAuthCookie(pageAuthToken, pageAuthExpiresAt);', ' window.addEventListener("focus", function () {', ' schedulePageAuthRenewal();', ' });', diff --git a/src/web/public/css/theme-custom.css b/src/web/public/css/theme-custom.css index 72032e7..ebd03f9 100644 --- a/src/web/public/css/theme-custom.css +++ b/src/web/public/css/theme-custom.css @@ -1743,6 +1743,7 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection { display: flex; align-items: center; justify-content: space-between; + flex-wrap: nowrap; gap: 0; min-height: 3.25rem; } @@ -1756,6 +1757,7 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection { .template-field-head strong { font-size: 0.95rem; + min-width: 0; } .template-field-head .chip { @@ -1869,6 +1871,7 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection { display: grid; gap: 0.5rem; padding-top: 0.25rem; + margin-bottom: 0.75rem; } .api-region-placeholder-title { @@ -1880,7 +1883,8 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection { } .api-region-sample-accordion { - padding: 0.85rem 1rem; + padding: 0.85rem 1rem 1rem; + margin-bottom: 0.75rem; border: 1px solid var(--bs-border-color); border-radius: 0.85rem; background: var(--bs-secondary-bg); diff --git a/src/web/public/js/regions/type/api.js b/src/web/public/js/regions/type/api.js index 89733c3..16706c9 100644 --- a/src/web/public/js/regions/type/api.js +++ b/src/web/public/js/regions/type/api.js @@ -191,6 +191,7 @@ var region = context.region; var current = context.current || {}; var config = context.config || {}; + var placeholderFields = Array.isArray(context.placeholderFields) ? context.placeholderFields : []; var placeholderChips = String(context.placeholderChips || ''); var sourceOptions = String(context.sourceOptions || ''); var itemsPathValue = context.itemsPath !== undefined @@ -234,7 +235,7 @@ '
Use transforms like {{name.upper()}}, {{name.title()}}, or {{name.lower()}} on leaf fields.
' + '
' + '
Available placeholders
' + - '
' + (placeholderChips || 'No JSON fields available.') + '
' + + '
' + ((placeholderFields.length && window.placeholderChips && typeof window.placeholderChips.renderChips === 'function') ? window.placeholderChips.renderChips(placeholderFields) : placeholderChips || 'No JSON fields available.') + '
' + '
' + '
' + 'Data' + diff --git a/src/web/public/js/regions/type/rss.js b/src/web/public/js/regions/type/rss.js index faaff7b..77f3252 100644 --- a/src/web/public/js/regions/type/rss.js +++ b/src/web/public/js/regions/type/rss.js @@ -218,7 +218,7 @@ 'RSS' + '' + '' + - '
' + + '
' + '
' + '' + '
' + @@ -237,8 +237,11 @@ '
' + '' + '' + - '
Use placeholders like {{title.upper()}}, {{title.title()}}, or {{title.lower()}}. Available placeholders:
' + - '
' + (placeholderChips || 'No RSS fields available.') + '
' + + '
Use transforms like {{title.upper()}}, {{title.title()}}, or {{title.lower()}} on leaf fields.
' + + '
' + + '
Available placeholders
' + + '
' + (placeholderChips || 'No RSS fields available.') + '
' + + '
' + '
' + ''; } diff --git a/src/web/public/js/regions/type/rtmp.js b/src/web/public/js/regions/type/rtmp.js index 04e72c9..d828fe9 100644 --- a/src/web/public/js/regions/type/rtmp.js +++ b/src/web/public/js/regions/type/rtmp.js @@ -21,7 +21,7 @@ function renderEditorCard(context) { var region = context.region; var current = String(context.current || ''); - var disableAudio = context.disableAudio === undefined ? true : Boolean(context.disableAudio); + var disableAudio = context.disableAudio === undefined ? false : Boolean(context.disableAudio); return '' + '
' + '
' + diff --git a/src/web/public/js/slides/slide-form-editor.js b/src/web/public/js/slides/slide-form-editor.js index 7a4d35e..b38ab54 100644 --- a/src/web/public/js/slides/slide-form-editor.js +++ b/src/web/public/js/slides/slide-form-editor.js @@ -233,7 +233,7 @@ export function createSlideFormEditorController(options) { promotion: false, statusbar: true, resize: true, - plugins: 'lists link code advlist fullscreen table', + plugins: 'lists code advlist fullscreen table', toolbar: 'undo redo | fontfamily fontsizeinput | forecolor backcolor bold italic underline strikethrough subscript superscript removeformat | align lineheight indent outdent bullist numlist table | fullscreen', toolbar_mode: 'sliding', license_key: 'gpl', @@ -244,6 +244,7 @@ export function createSlideFormEditorController(options) { content_style: 'body { font-family: ' + defaultEditorFontFamily + '; font-size: 32px; line-height: 1.5; background-color: ' + getEditorBackgroundColorValue() + '; } p { margin: 1em 0; } p:first-child { margin-top: 0; } p:last-child { margin-bottom: 1em; } table { border-collapse: collapse; width: 100%; } td, th { border: 1px solid currentColor; padding: 0.35em 0.5em; vertical-align: top; } th { font-weight: 700; }' + (editorContentStyle ? ' ' + editorContentStyle : ''), font_family_formats: getFontFamilyFormats(), font_size_input_default_unit: 'px', + invalid_elements: 'a', forced_root_block: 'p', force_br_newlines: false, newline_behavior: 'default', diff --git a/src/web/public/js/slides/slide-form-regions.js b/src/web/public/js/slides/slide-form-regions.js index 4488448..5bc2754 100644 --- a/src/web/public/js/slides/slide-form-regions.js +++ b/src/web/public/js/slides/slide-form-regions.js @@ -200,7 +200,16 @@ export function createSlideFormRegionHelpers(options) { function updateRssPlaceholderChips(regionId, feedId) { var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null; - updatePlaceholderChipList(card, getRssFieldList(feedId), 'No RSS fields available.'); + if (!card) { + return; + } + + var container = card.querySelector('[data-placeholder-chips]'); + if (!container) { + return; + } + + container.innerHTML = buildLimitedPlaceholderChipMarkup(getRssFieldList(feedId), 'No RSS fields available.'); } function getCurrentRssConfig(region) { @@ -502,6 +511,11 @@ export function createSlideFormRegionHelpers(options) { return '{{' + escapeHtml(field) + '}}'; }).join('') : buildLimitedPlaceholderChipMarkup(getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path), 'No JSON fields available.'), + placeholderFields: region.region_type === 'api' + ? getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path) + : region.region_type === 'rss' + ? getRssFieldList(getCurrentRssConfig(region).feed_id) + : [], sampleDataPreview: buildApiSampleDataMarkup(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).item_number, getCurrentApiConfig(region).items_path), timetableGroups: timetableGroups }, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : apiSources); @@ -533,10 +547,13 @@ export function createSlideFormRegionHelpers(options) { return ''; }).join(''), placeholderChips: region.region_type === 'rss' - ? getRssFieldList(rssConfig.feed_id).map(function (field) { - return '{{' + escapeHtml(field) + '}}'; - }).join('') + ? buildLimitedPlaceholderChipMarkup(getRssFieldList(rssConfig.feed_id), 'No RSS fields available.') : buildLimitedPlaceholderChipMarkup(getApiFieldList(apiConfig.source_id, apiItemsPath), 'No JSON fields available.'), + placeholderFields: region.region_type === 'api' + ? getApiFieldList(apiConfig.source_id, apiItemsPath) + : region.region_type === 'rss' + ? getRssFieldList(rssConfig.feed_id) + : [], sampleDataPreview: buildApiSampleDataMarkup(apiConfig.source_id, apiConfig.item_number, apiItemsPath), timetableGroups: timetableGroups }; diff --git a/src/web/views/signage/templates/form.hbs b/src/web/views/signage/templates/form.hbs index ed8f321..4bc28f8 100644 --- a/src/web/views/signage/templates/form.hbs +++ b/src/web/views/signage/templates/form.hbs @@ -138,7 +138,7 @@