diff --git a/CHANGELOG.md b/CHANGELOG.md index 5951ee6..c53e473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## 2.17.4 - 2026-10-23 + +### Added + +- API regions can sort array results by a selected field in ascending or descending order, independently per region. +- API placeholders can reference array entries by zero-based index, such as `{{0.name}}` and `{{1.name}}`; plain field placeholders continue to work with single-object responses. +- API result previews show the first item as an expandable JSON tree. + +### Changed + +- Image dimension guides appear in the small editor preview but not in the popup preview. + ## 2.17.3 - 2026-10-23 ### Fixed @@ -13,7 +25,7 @@ All notable changes to this project will be documented in this file. ### Added -- Added provider-specific weather normalization for Open-Meteo and Pirate Weather, giving current, hourly, and daily forecasts a shared format across previews, playback, and thumbnails, including cached responses, condition icons, timezone-aware dates, and independently selected display units. +- Provider-specific weather normalization for Open-Meteo and Pirate Weather, giving current, hourly, and daily forecasts a shared format across previews, playback, and thumbnails, including cached responses, condition icons, timezone-aware dates, and independently selected display units. ### Fixed @@ -30,9 +42,9 @@ All notable changes to this project will be documented in this file. ### Added -- Added read-only user previews from the Users page, controlled by Impersonate > Allow, with an expiring session, a persistent exit banner, and audit logging. -- Added a responsive changelog page linked from About, with collapsible release history and the latest release expanded by default. -- Added a password-only screen for required password changes, keeping profile and session controls unavailable until completion. +- Read-only user previews from the Users page, controlled by Impersonate > Allow, with an expiring session, a persistent exit banner, and audit logging. +- A responsive changelog page linked from About, with collapsible release history and the latest release expanded by default. +- A password-only screen for required password changes, keeping profile and session controls unavailable until completion. - Dashboard-wide reload, pause, and blackout actions use Connected clients > Allow, the same permission as per-client commands. - Signed-in users without Dashboard access now land on their first permitted section; users without assigned sections get a no-access page with Account and Sign out options. - Role permission checkboxes show each permission's description on hover. @@ -41,7 +53,7 @@ All notable changes to this project will be documented in this file. ### Added -- Added a responsive changelog page linked from About, with collapsible release history and the latest release expanded by default. +- A responsive changelog page linked from About, with collapsible release history and the latest release expanded by default. ## 2.16.7 - 2026-10-22 @@ -88,7 +100,7 @@ All notable changes to this project will be documented in this file. ### Added -- Added an API source Enable/Disable action in the Connection card that pauses all request refreshes while preserving each request's individual enabled setting and unsaved form changes. +- An API source Enable/Disable action in the Connection card that pauses all request refreshes while preserving each request's individual enabled setting and unsaved form changes. ### Fixed @@ -101,19 +113,19 @@ All notable changes to this project will be documented in this file. ### Added -- Added API source groups with shared authentication and cached tokens, plus support for unauthenticated public APIs. Existing request IDs and slide bindings are preserved. -- Added individually named request cards with independent URLs, enabled states, refresh schedules, and full-width response trees with Expand all/Collapse all controls. The Connection card provides source actions and Add request, with confirmation before removing a request. -- Added group-level authentication testing and per-request diagnostics using unsaved settings, with dismissible results below the tested section. Diagnostic reports redact credentials and populated tokens while preserving OAuth metadata and null or empty token fields. +- API source groups with shared authentication and cached tokens, plus support for unauthenticated public APIs. Existing request IDs and slide bindings are preserved. +- Individually named request cards with independent URLs, enabled states, refresh schedules, and full-width response trees with Expand all/Collapse all controls. The Connection card provides source actions and Add request, with confirmation before removing a request. +- Group-level authentication testing and per-request diagnostics using unsaved settings, with dismissible results below the tested section. Diagnostic reports redact credentials and populated tokens while preserving OAuth metadata and null or empty token fields. ## 2.16.0 - 2026-10-20 ### Added -- Added a slide layout editor with drag/resize, pixel position and size, z-index, ratio locking, keyboard nudging, undo/redo, and reset-to-template controls. Its wide modal leaves the sidebar visible and matches the template editor's region outlines; both editors provide alignment guides and snapping, with the template snap toggle in the Designer header. -- Added slide-specific region geometry overrides that flow through previews, playback, and thumbnails without changing sibling slides. Users with template-edit permission can apply the layout to this slide or save it to the shared template; shared saves confirm the affected-slide count, preserve region IDs and animations, clear only the current slide's geometry overrides, and reject stale templates. -- Added a separate slide animation editor with the template's presets and advanced timing controls, plus intro/outro/attention previews on the actual slide canvas. Changes can be applied to this slide or saved to the shared template; per-region and all-region reset actions are available, and shared saves preserve slide content and sibling overrides. -- Added consistent +/- sizing controls to HTML region editors, matching WYSIWYG regions, with the sandboxed iframe note alongside the controls. -- Added an API source connection test using unsaved form values, with separate authentication, data request, and JSON/items checks, sanitised response previews, available field paths, and a copyable diagnostic report in a dismissible results card without changing stored responses. +- A slide layout editor with drag/resize, pixel position and size, z-index, ratio locking, keyboard nudging, undo/redo, and reset-to-template controls. Its wide modal leaves the sidebar visible and matches the template editor's region outlines; both editors provide alignment guides and snapping, with the template snap toggle in the Designer header. +- Slide-specific region geometry overrides that flow through previews, playback, and thumbnails without changing sibling slides. Users with template-edit permission can apply the layout to this slide or save it to the shared template; shared saves confirm the affected-slide count, preserve region IDs and animations, clear only the current slide's geometry overrides, and reject stale templates. +- A separate slide animation editor with the template's presets and advanced timing controls, plus intro/outro/attention previews on the actual slide canvas. Changes can be applied to this slide or saved to the shared template; per-region and all-region reset actions are available, and shared saves preserve slide content and sibling overrides. +- Consistent +/- sizing controls to HTML region editors, matching WYSIWYG regions, with the sandboxed iframe note alongside the controls. +- An API source connection test using unsaved form values, with separate authentication, data request, and JSON/items checks, sanitised response previews, available field paths, and a copyable diagnostic report in a dismissible results card without changing stored responses. ### Changed @@ -123,8 +135,8 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed region dragging and resizing when template animation metadata is null. -- Fixed template and slide region outlines so they follow sizes below 12px, including scaled one-pixel regions, while keeping resize handles usable. +- Region dragging and resizing when template animation metadata is null. +- Template and slide region outlines so they follow sizes below 12px, including scaled one-pixel regions, while keeping resize handles usable. ## 2.15.7 - 2026-10-19 @@ -142,8 +154,8 @@ All notable changes to this project will be documented in this file. ### Added -- Added an All timetable display mode that includes past items while respecting date filters and the item limit. -- Added filename renaming in Media Library image and video previews without changing stored media paths or slide references. +- An All timetable display mode that includes past items while respecting date filters and the item limit. +- Filename renaming in Media Library image and video previews without changing stored media paths or slide references. ### Changed @@ -165,8 +177,8 @@ All notable changes to this project will be documented in this file. ### Added -- Added configurable audit events for media-library assets, managed fonts, and weather icon sets. -- Added All / Between Dates / Before Date / After Date filtering to timetable regions, with optional inclusive start-time boundaries shared by previews and playback. +- Configurable audit events for media-library assets, managed fonts, and weather icon sets. +- All / Between Dates / Before Date / After Date filtering to timetable regions, with optional inclusive start-time boundaries shared by previews and playback. ### Changed @@ -197,13 +209,13 @@ All notable changes to this project will be documented in this file. ### Added -- Added Pick / Repeat group modes beside the group title. Repeat plays the entire eligible group in order for the configured number of passes, independently of Pick and shuffle settings. +- Pick / Repeat group modes beside the group title. Repeat plays the entire eligible group in order for the configured number of passes, independently of Pick and shuffle settings. ## 2.15.1 - 2026-10-15 ### Added -- Added an optional shuffle toggle for playlist groups; ordered rotation is now the default. +- An optional shuffle toggle for playlist groups; ordered rotation is now the default. ### Changed @@ -214,7 +226,7 @@ All notable changes to this project will be documented in this file. ### Added -- Added playlist groups with configurable per-pass selection counts and independent multi-rule display schedules. +- Playlist groups with configurable per-pass selection counts and independent multi-rule display schedules. ## 2.14.3 - 2026-10-13 @@ -226,7 +238,7 @@ All notable changes to this project will be documented in this file. ### Added -- Added managed weather icon sets under Settings > Assets, with day and optional night variants, PNG/WebP/SVG uploads, per-region selection, and player synchronization. +- Managed weather icon sets under Settings > Assets, with day and optional night variants, PNG/WebP/SVG uploads, per-region selection, and player synchronization. ### Changed @@ -236,7 +248,7 @@ All notable changes to this project will be documented in this file. ### Added -- Added a configurable text size for chart axis labels, legends, and tooltips. +- A configurable text size for chart axis labels, legends, and tooltips. ### Changed @@ -259,10 +271,10 @@ All notable changes to this project will be documented in this file. ### Added -- Added configurable chart data sources, multi-series line charts, chart type and source options, value ordering, grid line colors, and text colors across editor, popout preview, and player rendering. -- Added player playlist and playback recovery for cached snapshots, offline states, stalled media, failed image/video loads, and service-worker cache failures. -- Added deterministic image derivatives for large uploads while preserving the original file and preventing generated variants from exceeding the original dimensions or file size. -- Added immediate template background uploads with shared media validation, variant generation, cleanup of abandoned uploads, and permission checks. +- Configurable chart data sources, multi-series line charts, chart type and source options, value ordering, grid line colors, and text colors across editor, popout preview, and player rendering. +- Player playlist and playback recovery for cached snapshots, offline states, stalled media, failed image/video loads, and service-worker cache failures. +- Deterministic image derivatives for large uploads while preserving the original file and preventing generated variants from exceeding the original dimensions or file size. +- Immediate template background uploads with shared media validation, variant generation, cleanup of abandoned uploads, and permission checks. ### Changed @@ -280,13 +292,13 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed the template background image media picker Upload media button so it opens the file selector. -- Fixed the template designer empty background message moving when the background image is removed. -- Fixed cached API and RSS images not being served by the player after being downloaded. -- Fixed blank time/date regions displaying the default clock format on the player. -- Fixed the time/date placeholder popup omitting text and math transform help. -- Fixed empty region previews displaying type labels instead of remaining blank. -- Fixed empty weather regions displaying the default weather summary. +- The template background image media picker Upload media button so it opens the file selector. +- The template designer empty background message moving when the background image is removed. +- Cached API and RSS images not being served by the player after being downloaded. +- Blank time/date regions displaying the default clock format on the player. +- The time/date placeholder popup omitting text and math transform help. +- Empty region previews displaying type labels instead of remaining blank. +- Empty weather regions displaying the default weather summary. ## 2.13.2 - 2026-09-11 @@ -298,7 +310,7 @@ All notable changes to this project will be documented in this file. ### Added -- Added the shared Media Library to slide region controls and template editors, preserving existing media paths while providing safe private uploads, responsive four-row server-side batching, and card-only scrolling with Load more pagination. +- The shared Media Library to slide region controls and template editors, preserving existing media paths while providing safe private uploads, responsive four-row server-side batching, and card-only scrolling with Load more pagination. ### Changed @@ -310,9 +322,9 @@ All notable changes to this project will be documented in this file. ### Added -- Added offline-capable Local Control for central users with `clients.allow`, scoped to each player and its connected clients, with responsive client actions, current slide titles, and live WebSocket updates for pause and blackout state changes. -- Added player-specific HTTP-only sessions, minimized cached authentication data with hashed usernames, immediate session and socket revocation when authorization changes, and login throttling after repeated failures. -- Added web-owned authorization synchronization: connected remote players refresh every fifteen minutes and receive the current authorization state immediately after reconnecting, while the configured local player is excluded from remote fanout. +- Offline-capable Local Control for central users with `clients.allow`, scoped to each player and its connected clients, with responsive client actions, current slide titles, and live WebSocket updates for pause and blackout state changes. +- Player-specific HTTP-only sessions, minimized cached authentication data with hashed usernames, immediate session and socket revocation when authorization changes, and login throttling after repeated failures. +- Web-owned authorization synchronization: connected remote players refresh every fifteen minutes and receive the current authorization state immediately after reconnecting, while the configured local player is excluded from remote fanout. ## 2.11.3 - 2026-09-08 @@ -382,7 +394,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed client screen moves from mobile cards so the next move disables the client’s current screen group rather than its previous one. +- Client screen moves from mobile cards so the next move disables the client’s current screen group rather than its previous one. - Removed unused Bootstrap Icons font preloads that triggered browser warnings on pages where icons were not rendered. ## 2.10.6 - 2026-08-29 @@ -398,23 +410,23 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed remote player commands and media synchronization so they route through the bridge using the physical player device identity, including announcement and playlist refresh notifications. -- Fixed player media handling for remote bridge uploads and deletes by using the bridge endpoint with explicit player-device authentication. -- Fixed player service-worker caching so ranged media requests bypass stale cached responses and video playback remains reliable. +- Remote player commands and media synchronization so they route through the bridge using the physical player device identity, including announcement and playlist refresh notifications. +- Player media handling for remote bridge uploads and deletes by using the bridge endpoint with explicit player-device authentication. +- Player service-worker caching so ranged media requests bypass stale cached responses and video playback remains reliable. ## 2.10.5 - 2026-08-29 ### Fixed -- Fixed intermittent pairing failures while player registration and pairing sessions propagate through the bridge by retrying transient resolution and completion requests. -- Fixed the pairing page so transient submission failures retry automatically and pairing progress uses a single spinner without dimming the form. -- Fixed the player onboarding page so it continues polling until the pairing code and QR code are available. +- Intermittent pairing failures while player registration and pairing sessions propagate through the bridge by retrying transient resolution and completion requests. +- The pairing page so transient submission failures retry automatically and pairing progress uses a single spinner without dimming the form. +- The player onboarding page so it continues polling until the pairing code and QR code are available. ## 2.10.4 - 2026-08-29 ### Fixed -- Fixed remote client move commands so they route through the player bridge to the correct browser tab using the physical player identity and connection ID. +- Remote client move commands so they route through the player bridge to the correct browser tab using the physical player identity and connection ID. - Removed the obsolete `PLAYER_BASE_URL` configuration fallback; command routing uses `PLAYER_INTERNAL_URL` locally or the player bridge remotely, while `PLAYER_PUBLIC_URL` remains available for kiosk launcher and direct player access. - Removed the player URL list from screen-group add/edit pages and expanded the remaining form card to full width. @@ -427,19 +439,19 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed the weather edit preview so the daily forecast is shown initially, the 24-hour forecast is hidden until selected, and the redundant hourly heading is removed. -- Fixed onboarding cleanup so incomplete pairings are retained for the 15-minute pairing-code lifetime while inactive completed bindings are pruned after 24 hours. -- Fixed client-name handling so offline names can be reused, active collisions receive numeric suffixes, and reconnecting players resolve duplicate names consistently. -- Fixed an internal server error when renaming clients by forwarding the available-name resolver to the client command routes. -- Fixed remote client moves to resolve the paired browser client binding before changing its target screen. -- Fixed remote player heartbeats to update the central onboarding bindings for active browser clients without treating the physical player registry ID as a client binding. -- Fixed targeted commands after browser reconnects by falling back to the stable client ID when a transient connection ID is stale. -- Fixed stale browser command connections remaining active indefinitely when the physical player heartbeat was still healthy. -- Fixed media synchronization so generated player caches are excluded while remote image caches remain available to players. -- Fixed weather screen notifications so changes in the fetched data or the current forecast hour trigger a refresh. -- Fixed player runtime script loading and slide rendering so region modules, transitions, and cached playlists initialize consistently. -- Fixed command delivery reporting to require acknowledgement from the receiving browser tab. -- Fixed playlist refresh notifications to target the physical player hosting each screen instead of always targeting the local player. +- The weather edit preview so the daily forecast is shown initially, the 24-hour forecast is hidden until selected, and the redundant hourly heading is removed. +- Onboarding cleanup so incomplete pairings are retained for the 15-minute pairing-code lifetime while inactive completed bindings are pruned after 24 hours. +- Client-name handling so offline names can be reused, active collisions receive numeric suffixes, and reconnecting players resolve duplicate names consistently. +- An internal server error when renaming clients by forwarding the available-name resolver to the client command routes. +- Remote client moves to resolve the paired browser client binding before changing its target screen. +- Remote player heartbeats to update the central onboarding bindings for active browser clients without treating the physical player registry ID as a client binding. +- Targeted commands after browser reconnects by falling back to the stable client ID when a transient connection ID is stale. +- Stale browser command connections remaining active indefinitely when the physical player heartbeat was still healthy. +- Media synchronization so generated player caches are excluded while remote image caches remain available to players. +- Weather screen notifications so changes in the fetched data or the current forecast hour trigger a refresh. +- Player runtime script loading and slide rendering so region modules, transitions, and cached playlists initialize consistently. +- Command delivery reporting to require acknowledgement from the receiving browser tab. +- Playlist refresh notifications to target the physical player hosting each screen instead of always targeting the local player. ## 2.10.2 - 2026-08-28 @@ -449,7 +461,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed the weather forecast preview to show only its first-fetch message until a successful forecast is available. +- The weather forecast preview to show only its first-fetch message until a successful forecast is available. ## 2.10.1 - 2026-08-28 @@ -527,7 +539,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed thumbnail capture timing so video assets are ready before the preview canvas is captured. +- Thumbnail capture timing so video assets are ready before the preview canvas is captured. - Replaced unavailable webpage thumbnails with a subdued placeholder while keeping live webpage previews intact. ## 2.8.4 - 2026-08-17 @@ -539,9 +551,9 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed popup preview authentication for background thumbnail capture. -- Fixed thumbnails so they use the same popup-preview canvas and resolve API/RSS placeholders, fonts, styles, and cached images correctly. -- Fixed manual scheduled-task run notifications so they use task-neutral wording. +- Popup preview authentication for background thumbnail capture. +- Thumbnails so they use the same popup-preview canvas and resolve API/RSS placeholders, fonts, styles, and cached images correctly. +- Manual scheduled-task run notifications so they use task-neutral wording. ## 2.8.3 - 2026-08-17 @@ -1217,7 +1229,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Removed several onboarding and runtime code paths that were still inserting or refreshing `d_players` rows during normal player playback. -- Fixed the screen-binding flow so it no longer writes a device UUID into `d_screens.player_id`, which avoided foreign key errors under the shared-player model. +- The screen-binding flow so it no longer writes a device UUID into `d_screens.player_id`, which avoided foreign key errors under the shared-player model. ## 2.0.0 - 2026-07-28 @@ -1356,7 +1368,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed a migration gap that could leave `slides.thumbnail_path` missing on upgraded databases. +- A migration gap that could leave `slides.thumbnail_path` missing on upgraded databases. ## 1.5.4 - 2026-07-25 @@ -1393,13 +1405,13 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed player media upload sync so signed upload requests are verified after the request body has been parsed. +- Player media upload sync so signed upload requests are verified after the request body has been parsed. ## 1.5.1 - 2026-07-25 ### Fixed -- Fixed the account page so it can save changes again. +- The account page so it can save changes again. ## 1.5.0 - 2026-07-25 @@ -1452,13 +1464,13 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed RBAC client action visibility. +- RBAC client action visibility. ## 1.4.2 - 2026-07-21 ### Fixed -- Fixed duplicate permissions cleanup. +- Duplicate permissions cleanup. ## 1.4.1 - 2026-07-21 @@ -1478,7 +1490,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed duplicate onboarding client names. +- Duplicate onboarding client names. ## 1.3.3 - 2026-07-21 @@ -1514,20 +1526,20 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed screen playlist reassignment so changing a screen's playlist now triggers a refresh. -- Fixed single-slide playlist refresh behavior so updates do not get stuck behind the current slide. +- Screen playlist reassignment so changing a screen's playlist now triggers a refresh. +- Single-slide playlist refresh behavior so updates do not get stuck behind the current slide. ## 1.1.5 - 2026-07-15 ### Fixed -- Fixed text pasting in the admin UI. +- Text pasting in the admin UI. ## 1.1.4 - 2026-07-15 ### Fixed -- Fixed the admin table and playlist reorder UI. +- The admin table and playlist reorder UI. ## 1.1.3 - 2026-07-15 diff --git a/build/package.player.json b/build/package.player.json index 60f836e..3531d50 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.17.3", + "version": "2.17.4", "private": false, "description": "Pulse Signage player application bundle", "engines": { diff --git a/build/package.web.json b/build/package.web.json index 0270d03..a7ceec9 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.17.3", + "version": "2.17.4", "private": false, "description": "Pulse Signage web and bridge application bundle", "engines": { diff --git a/package-lock.json b/package-lock.json index 180c914..93c4322 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-signage", - "version": "2.17.3", + "version": "2.17.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.17.3", + "version": "2.17.4", "dependencies": { "animate.css": "^4.1.1", "bootstrap-icons": "1.13.1", diff --git a/package.json b/package.json index 8cf7455..f035f1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.17.3", + "version": "2.17.4", "private": false, "description": "Pulse Signage application with MySQL and media storage", "engines": { diff --git a/src/data/slides.js b/src/data/slides.js index ff43ec8..a8f6e94 100644 --- a/src/data/slides.js +++ b/src/data/slides.js @@ -466,6 +466,9 @@ async function buildTemplateContent(pool, template, body, filesByField, existing const sourceId = body[`region_api_source_id_${region.id}`]; const itemNumber = body[`region_api_item_number_${region.id}`]; const itemsPath = body[`region_api_items_path_${region.id}`]; + const sortMode = body[`region_api_sort_mode_${region.id}`]; + const sortBy = body[`region_api_sort_by_${region.id}`]; + const sortDirection = body[`region_api_sort_direction_${region.id}`]; const parsedItemNumber = Math.max(1, Number(itemNumber || current.item_number || 1)); content[region.region_key] = { type: 'api', @@ -473,6 +476,8 @@ async function buildTemplateContent(pool, template, body, filesByField, existing source_id: sourceId === undefined || sourceId === null ? (current.source_id || null) : (sourceId === '' ? null : Number(sourceId)), item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1, items_path: itemsPath === undefined || itemsPath === null ? (current.items_path === undefined || current.items_path === null ? '' : String(current.items_path)) : String(itemsPath || '').trim(), + sort_by: sortMode === undefined ? String(current.sort_by || '') : (sortMode === 'field' ? String(sortBy || '').trim() : ''), + sort_direction: sortDirection === undefined ? (String(current.sort_direction || '').toLowerCase() === 'desc' ? 'desc' : 'asc') : (sortDirection === 'desc' ? 'desc' : 'asc'), variable_name: 'item', font_family: style.font_family, font_size: style.font_size, diff --git a/src/player/playlist.js b/src/player/playlist.js index 921c15c..39cda48 100644 --- a/src/player/playlist.js +++ b/src/player/playlist.js @@ -5,7 +5,7 @@ const { convertWeatherSnapshot } = require('#src/data/weather-units'); const fs = require('fs'); const path = require('path'); const { createStyledQrCodeDataUrl } = require('#src/data/qr-code'); -const { resolveApiItems } = require('#src/web/public/js/shared/placeholder-utils'); +const { resolveApiItems, sortApiItems } = require('#src/web/public/js/shared/placeholder-utils'); const IMAGE_VARIANT_DIMENSIONS = [2048, 4096, 8192, 16384]; @@ -157,8 +157,14 @@ function createPlayerPlaylistService(options) { const itemNumber = Math.max(1, Number(region.item_number || 1)) - 1; if (String(region.type || '').toLowerCase() === 'api') { const source = (apiSources || []).find(function (entry) { return Number(entry.id) === Number(region.source_id); }); - const items = source ? resolveApiItems(source.responseJson, region.items_path === undefined || region.items_path === null ? source.items_path : region.items_path) : []; - await cacheSource(source, items[itemNumber], expressions); + const items = source ? sortApiItems(resolveApiItems(source.responseJson, region.items_path === undefined || region.items_path === null ? source.items_path : region.items_path), region.sort_by, region.sort_direction) : []; + for (const expression of expressions) { + const indexedMatch = String(expression || '').match(/^\s*(\d+)\.(.+)$/); + const resultIndex = indexedMatch ? Number(indexedMatch[1]) : 0; + const targetItem = indexedMatch ? items[resultIndex] : items[itemNumber]; + const targetExpression = indexedMatch ? indexedMatch[2] : expression; + await cacheSource(source, targetItem, [targetExpression]); + } } if (String(region.type || '').toLowerCase() === 'rss') { const feed = (rssFeeds || []).find(function (entry) { return Number(entry.id) === Number(region.feed_id); }); diff --git a/src/player/regions/api.js b/src/player/regions/api.js index a787ea1..9db93b2 100644 --- a/src/player/regions/api.js +++ b/src/player/regions/api.js @@ -19,24 +19,29 @@ function getApiSourceItemsPath(source, overridePath) { return String(overridePath || '').trim(); } -function getApiSourceItems(sourceId, itemsPathOverride) { +function getApiSourceItems(sourceId, itemsPathOverride, sortBy, sortDirection) { var source = getApiSourceById(sourceId); var responseJson = source && source.responseJson && typeof source.responseJson === 'object' ? source.responseJson : null; var itemsPath = getApiSourceItemsPath(source, itemsPathOverride); - return placeholderUtils.resolveApiItems(responseJson, itemsPath); + var items = placeholderUtils.resolveApiItems(responseJson, itemsPath); + return typeof placeholderUtils.sortApiItems === 'function' ? placeholderUtils.sortApiItems(items, sortBy, sortDirection) : items; } -function getApiItem(sourceId, itemNumber, itemsPathOverride) { - var items = getApiSourceItems(sourceId, itemsPathOverride); +function getApiItem(sourceId, itemNumber, itemsPathOverride, sortBy, sortDirection) { + var items = getApiSourceItems(sourceId, itemsPathOverride, sortBy, sortDirection); var parsedItemNumber = Math.max(1, Number(itemNumber || 1)); var index = Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber - 1 : 0; return items[index] || null; } -function substituteApiVariables(html, item, sourceId) { +function substituteApiVariables(html, item, sourceId, items) { var source = String(html || ''); return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) { - if (!item || typeof item !== 'object') { + var indexedMatch = String(expression || '').match(/^\s*(\d+)\.(.+)$/); + var resultIndex = indexedMatch ? Number(indexedMatch[1]) : 0; + var expressionItem = indexedMatch ? (Array.isArray(items) ? items[resultIndex] : null) : item; + var resolvedExpression = indexedMatch ? indexedMatch[2] : expression; + if (!expressionItem || typeof expressionItem !== 'object') { return ''; } @@ -44,18 +49,18 @@ function substituteApiVariables(html, item, sourceId) { return ''; } - var resolved = placeholderUtils.resolvePlaceholderExpression(item, expression); - if (typeof placeholderUtils.isProgressPlaceholderExpression === 'function' && placeholderUtils.isProgressPlaceholderExpression(expression)) { - return placeholderUtils.renderProgressPlaceholder(item, expression); + var resolved = placeholderUtils.resolvePlaceholderExpression(expressionItem, resolvedExpression); + if (typeof placeholderUtils.isProgressPlaceholderExpression === 'function' && placeholderUtils.isProgressPlaceholderExpression(resolvedExpression)) { + return placeholderUtils.renderProgressPlaceholder(expressionItem, resolvedExpression); } - if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(expression)) { + if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(resolvedExpression)) { var imageSource = placeholderUtils.formatPlaceholderValue(resolved); var source = getApiSourceById(sourceId); imageSource = source && source.imageCache && source.imageCache[imageSource] ? source.imageCache[imageSource] : imageSource; if (!/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/])/i.test(imageSource)) { return ''; } - var imageConfig = typeof placeholderUtils.getImagePlaceholderConfig === 'function' ? placeholderUtils.getImagePlaceholderConfig(expression) : {}; + var imageConfig = typeof placeholderUtils.getImagePlaceholderConfig === 'function' ? placeholderUtils.getImagePlaceholderConfig(resolvedExpression) : {}; var hasImageBounds = imageConfig.width && imageConfig.height; var imageStyle = hasImageBounds ? 'display:block;width:100%;height:100%;object-fit:contain;' @@ -102,12 +107,17 @@ function renderApiRegion(region, regionContent) { var sourceId = regionContent && regionContent.source_id !== undefined ? regionContent.source_id : null; var itemNumber = regionContent && regionContent.item_number !== undefined ? regionContent.item_number : 1; var itemsPath = regionContent && regionContent.items_path !== undefined ? regionContent.items_path : ''; + var sortBy = regionContent && regionContent.sort_by !== undefined ? regionContent.sort_by : ''; + var sortDirection = regionContent && regionContent.sort_direction !== undefined ? regionContent.sort_direction : 'asc'; var fontFamily = sanitizeFontFamily(regionContent.font_family || region.fontFamily); var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize); var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor); - var item = getApiItem(sourceId, itemNumber, itemsPath); + var items = getApiSourceItems(sourceId, itemsPath, sortBy, sortDirection); + var parsedItemNumber = Math.max(1, Number(itemNumber || 1)); + var itemIndex = Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber - 1 : 0; + var item = items[itemIndex] || null; var hasSource = String(sourceId === undefined || sourceId === null ? '' : sourceId).trim() !== ''; - var body = hasSource ? (item ? substituteApiVariables(content, item, sourceId) : '') : content; + var body = hasSource ? (item || items.length ? substituteApiVariables(content, item, sourceId, items) : '') : content; var contentStyle = 'width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;transform:scale(' + region.canvasScale + ');transform-origin:top left;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';'; if (!body) { return ''; diff --git a/src/web/public/css/theme-custom.css b/src/web/public/css/theme-custom.css index 5649843..5f0d785 100644 --- a/src/web/public/css/theme-custom.css +++ b/src/web/public/css/theme-custom.css @@ -3716,47 +3716,81 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection { .api-region-placeholder-section { display: grid; - gap: 0.5rem; - padding-top: 0.25rem; - padding-bottom: 0.5rem; + gap: 0.4rem; + padding-top: 0.125rem; + padding-bottom: 0.25rem; margin-bottom: 0; } .api-region-placeholder-title { - color: var(--bs-primary); - font-size: 0.8rem; - font-weight: 800; - letter-spacing: 0.06em; - text-transform: uppercase; + color: var(--bs-secondary-color); + font-size: 0.78rem; + font-weight: 600; } .api-region-placeholder-title-help { - font-size: 0.9rem; - font-weight: 800; + font-size: 0.8rem; + font-weight: 600; letter-spacing: normal; text-transform: none; } -.api-region-sample-accordion { - padding: 0.85rem 1rem 1rem; - margin-bottom: 0.75rem; +.api-region-placeholder-section .chip { + padding: 0.2rem 0.45rem; border: 1px solid var(--bs-border-color); - border-radius: 0.85rem; - background: var(--bs-secondary-bg); + border-radius: 0.3rem; + background: transparent; + color: var(--bs-secondary-color); + font-family: var(--bs-font-monospace); + font-size: 0.75rem; } -.api-region-sample-accordion > summary { +.api-region-placeholder-section .chip:hover { + border-color: var(--bs-secondary-color); + background: var(--bs-tertiary-bg); +} + +.api-region-placeholder-section .chip:focus-visible { + outline: 2px solid var(--bs-primary); + outline-offset: 2px; +} + +.api-region-sample-accordion { + margin-bottom: 0.75rem; +} + +.api-region-sample-header { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.api-region-sample-toggle { + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + box-sizing: border-box; + height: 2.5rem; + padding: 0.375rem 0.75rem; cursor: pointer; - list-style: none; - color: var(--bs-primary); - font-size: 0.8rem; + font-size: 0.875rem; font-weight: 800; - letter-spacing: 0.06em; - text-transform: uppercase; + text-align: left; } -.api-region-sample-accordion > summary::-webkit-details-marker { - display: none; +.api-region-sample-toggle:focus-visible { + outline: 2px solid var(--bs-primary); + outline-offset: 2px; +} + +.api-region-sample-chevron { + transition: transform 120ms ease; +} + +.api-region-sample-accordion.is-open .api-region-sample-chevron { + transform: rotate(180deg); } .api-region-sample-body { @@ -3765,19 +3799,21 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection { margin-top: 0.75rem; } +.api-region-sample-body[hidden] { + display: none; +} + .api-region-sample-meta { color: var(--bs-secondary-color); font-size: 0.78rem; } .api-region-sample-preview { - max-height: 12rem; - overflow: auto; margin: 0; padding: 0.75rem; - border-radius: 0.65rem; - background: var(--bs-body-bg); - border: 1px solid var(--bs-border-color-translucent); + border: 0; + border-radius: 0.375rem; + background: var(--bs-secondary-bg); white-space: pre-wrap; word-break: break-word; font-size: 0.78rem; diff --git a/src/web/public/js/regions/type/api.js b/src/web/public/js/regions/type/api.js index 7281435..cdfe735 100644 --- a/src/web/public/js/regions/type/api.js +++ b/src/web/public/js/regions/type/api.js @@ -47,15 +47,16 @@ return String(overridePath || '').trim(); } - function getItems(sourceId, sources, itemsPathOverride) { + function getItems(sourceId, sources, itemsPathOverride, sortBy, sortDirection) { var source = getSourceById(sourceId, sources); var responseJson = source && source.responseJson && typeof source.responseJson === 'object' ? source.responseJson : null; var itemsPath = getSourceItemsPath(source, itemsPathOverride); - return placeholderUtils.resolveApiItems(responseJson, itemsPath); + var items = placeholderUtils.resolveApiItems(responseJson, itemsPath); + return typeof placeholderUtils.sortApiItems === 'function' ? placeholderUtils.sortApiItems(items, sortBy, sortDirection) : items; } - function getItemCount(sourceId, sources, itemsPathOverride) { - return getItems(sourceId, sources, itemsPathOverride).length; + function getItemCount(sourceId, sources, itemsPathOverride, sortBy, sortDirection) { + return getItems(sourceId, sources, itemsPathOverride, sortBy, sortDirection).length; } function getCurrentConfig(region, existingContent, sources) { @@ -66,15 +67,21 @@ source_id: current.source_id === undefined || current.source_id === null || current.source_id === '' ? '' : Number(current.source_id), item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1, items_path: Object.prototype.hasOwnProperty.call(current, 'items_path') ? String(current.items_path === undefined || current.items_path === null ? '' : current.items_path) : undefined, + sort_by: String(current.sort_by || ''), + sort_direction: String(current.sort_direction || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc', default_items_path: String(source && (source.items_path || source.itemsPath) || '').trim(), value: current.value !== undefined ? current.value : '' }; } - function substituteVariables(html, item, options) { + function substituteVariables(html, item, options, items) { var source = String(html || ''); return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) { - if (!item || typeof item !== 'object') { + var indexedMatch = String(expression || '').match(/^\s*(\d+)\.(.+)$/); + var resultIndex = indexedMatch ? Number(indexedMatch[1]) : 0; + var expressionItem = indexedMatch ? (Array.isArray(items) ? items[resultIndex] : null) : item; + var resolvedExpression = indexedMatch ? indexedMatch[2] : expression; + if (!expressionItem || typeof expressionItem !== 'object') { return ''; } @@ -82,16 +89,16 @@ return ''; } - var resolved = placeholderUtils.resolvePlaceholderExpression(item, expression); - if (typeof placeholderUtils.isProgressPlaceholderExpression === 'function' && placeholderUtils.isProgressPlaceholderExpression(expression)) { - return placeholderUtils.renderProgressPlaceholder(item, expression); + var resolved = placeholderUtils.resolvePlaceholderExpression(expressionItem, resolvedExpression); + if (typeof placeholderUtils.isProgressPlaceholderExpression === 'function' && placeholderUtils.isProgressPlaceholderExpression(resolvedExpression)) { + return placeholderUtils.renderProgressPlaceholder(expressionItem, resolvedExpression); } - if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(expression)) { + if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(resolvedExpression)) { var imageSource = placeholderUtils.formatPlaceholderValue(resolved); if (!/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/])/i.test(imageSource)) { return ''; } - var imageConfig = typeof placeholderUtils.getImagePlaceholderConfig === 'function' ? placeholderUtils.getImagePlaceholderConfig(expression) : {}; + var imageConfig = typeof placeholderUtils.getImagePlaceholderConfig === 'function' ? placeholderUtils.getImagePlaceholderConfig(resolvedExpression) : {}; var imageStyle = options && options.preview && imageConfig.width && imageConfig.height ? 'display:block;width:100%;height:100%;object-fit:contain;' : 'display:block;width:auto;height:auto;' + (imageConfig.width ? 'max-width:' + imageConfig.width + 'px;' : '') + (imageConfig.height ? 'max-height:' + imageConfig.height + 'px;' : ''); @@ -99,13 +106,14 @@ var hasPreviewBounds = options && options.preview && imageConfig.width && imageConfig.height; if (hasPreviewBounds || imageConfig.verticalAlign) { var wrapperStyle = 'display:inline-flex;align-items:center;justify-content:center;'; + var wrapperClass = hasPreviewBounds ? ' class="api-image-placeholder-bounds"' : ''; if (hasPreviewBounds) { wrapperStyle += 'box-sizing:border-box;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;border:1px dashed rgba(120,120,120,.75);'; } if (imageConfig.verticalAlign) { wrapperStyle += 'vertical-align:' + imageConfig.verticalAlign + ';'; } - return '' + image + ''; + return '' + image + ''; } return image; } @@ -113,8 +121,8 @@ }); } - function getItem(sourceId, itemNumber, sources, itemsPathOverride) { - var items = getItems(sourceId, sources, itemsPathOverride); + function getItem(sourceId, itemNumber, sources, itemsPathOverride, sortBy, sortDirection) { + var items = getItems(sourceId, sources, itemsPathOverride, sortBy, sortDirection); var parsedItemNumber = Math.max(1, Number(itemNumber || 1)); var index = Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber - 1 : 0; return items[index] || null; @@ -149,8 +157,8 @@ '
Placeholders read values from the selected API item. Nested fields use dots.
' + + 'Use placeholders such as {{name}} for the selected result, or {{0.name}} and {{1.name}} to read sorted results by index. Indices start at 0. Nested fields use dots.
]*class="[^"]*api-region-sample-preview[^"]*"[^>]*>[\s\S]*?<\/pre>/gi, '')
@@ -239,17 +248,13 @@
'' +
'' + escapeHtml(truncateJsonPreview(item, 1100)) + ''; + var firstItemJson = ''; + try { + firstItemJson = JSON.stringify(items[0], null, 2); + } catch (_error) { + firstItemJson = String(items[0] === undefined || items[0] === null ? '' : items[0]); + } + var label = apiItemsPathIsArray(source, itemsPathOverride) ? 'Example result (first item)' : 'Example result'; + var jsonSource = JSON.stringify(firstItemJson).replace(//g, '\\u003e').replace(/&/g, '\\u0026'); + return '' + + '
Manage shared authentication, request URLs, and latest responses\.<\/p>/); assert.match(add, /data-api-source-test-url="\/data-sources\/api-sources\/test"/); assert.match(edit, /data-api-source-test-url="\/data-sources\/api-sources\/42\/test"/); + assert.match(add, /data-async-save-new-redirect="response-url"/); + assert.doesNotMatch(edit, /data-async-save-new-redirect|data-async-save-refresh-page/); for (const html of [add, edit]) { assert.match(html, /