Release v2.14.1
This commit is contained in:
1 parent
635e1a9942
commit
c3299abaac
174 files changed
+4483
-1687
No files matched your search
Vendored
+2
-2
@@ -1,8 +1,8 @@
|
||||
// Web-to-player websocket bridge and dashboard state synchronizer.
|
||||
|
||||
const { WebSocketServer, WebSocket } = require('ws');
|
||||
const { createDashboardStateService } = require('./lib/dashboard-state');
|
||||
const { createUploadSyncService } = require('./lib/media');
|
||||
const { createDashboardStateService } = require('#src/web/lib/dashboard-state');
|
||||
const { createUploadSyncService } = require('#src/web/lib/media/index');
|
||||
const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||
|
||||
function createWebBootstrap(options) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Entry point for the web auth helper modules.
|
||||
|
||||
module.exports = {
|
||||
createSessionService: require('./session').createSessionService,
|
||||
getRequestOrigin: require('./session').getRequestOrigin,
|
||||
rbacData: require('./rbac-data')
|
||||
createSessionService: require('#src/web/lib/auth/session').createSessionService,
|
||||
getRequestOrigin: require('#src/web/lib/auth/session').getRequestOrigin,
|
||||
rbacData: require('#src/web/lib/auth/rbac-data')
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('../../data-source-refresh');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('#src/web/lib/data-source-refresh');
|
||||
|
||||
const TASK = {
|
||||
taskType: 'data-source-refresh'
|
||||
|
||||
@@ -8,8 +8,8 @@ const TASK = {
|
||||
intervalMs: null
|
||||
};
|
||||
|
||||
const { normalizeIntervalMs } = require('../queue');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('../../data-source-refresh');
|
||||
const { normalizeIntervalMs } = require('#src/web/lib/background-tasks/queue');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('#src/web/lib/data-source-refresh');
|
||||
|
||||
function registerRecurringDataSourceRefreshes(options) {
|
||||
const pool = options && options.pool;
|
||||
|
||||
@@ -11,7 +11,7 @@ const TASK = {
|
||||
const {
|
||||
collectFontLibraryDirectoryUploadPaths,
|
||||
collectFontLibrarySyncOperations
|
||||
} = require('../../media/font-library');
|
||||
} = require('#src/web/lib/media/font-library');
|
||||
|
||||
function registerFontSweepTask(options) {
|
||||
const backgroundTaskQueue = options && options.backgroundTaskQueue;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { collectLocalControlUsers } = require('../../local-control-users');
|
||||
const { collectLocalControlUsers } = require('#src/web/lib/local-control-users');
|
||||
|
||||
const TASK = {
|
||||
key: 'player-control-sync',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('../../data-source-refresh');
|
||||
const { normalizeIntervalMs } = require('../queue');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('#src/web/lib/data-source-refresh');
|
||||
const { normalizeIntervalMs } = require('#src/web/lib/background-tasks/queue');
|
||||
|
||||
const TASK = {
|
||||
key: 'startup-data-source-refresh',
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
const { collectFontLibrarySyncOperations } = require('../../media/font-library');
|
||||
const { fetchPlayerRegistrations } = require('#src/data/player-registry');
|
||||
const { collectFontLibrarySyncOperations } = require('#src/web/lib/media/font-library');
|
||||
const { fetchPlayerRegistrations, isRecentPlayerRegistration } = require('#src/data/player-registry');
|
||||
|
||||
const TASK = {
|
||||
key: 'initial-font-sync',
|
||||
category: 'fonts'
|
||||
};
|
||||
|
||||
function isRecentPlayerRegistration(player, staleSeconds) {
|
||||
const lastSeenAt = player && player.last_seen_at;
|
||||
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
|
||||
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
|
||||
|
||||
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
|
||||
}
|
||||
// Queue a one-off font sync for each live player on startup. The task keeps the resolved player
|
||||
// metadata in the payload so retries and background workers do not need to rediscover the same URL.
|
||||
|
||||
function normalizePlayerMetadata(player) {
|
||||
const playerIdentifier = String(player && player.identifier || '').trim();
|
||||
|
||||
@@ -3,15 +3,11 @@ const TASK = {
|
||||
category: 'media-sync',
|
||||
};
|
||||
|
||||
const { fetchPlayerRegistrations } = require('#src/data/player-registry');
|
||||
// Queue a one-off media sync for each live player when the web app starts up. We intentionally
|
||||
// do the target selection here rather than inside the task itself so the task payload remains
|
||||
// explicit and reusable for later retries.
|
||||
|
||||
function isRecentPlayerRegistration(player, staleSeconds) {
|
||||
const lastSeenAt = player && player.last_seen_at;
|
||||
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
|
||||
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
|
||||
|
||||
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
|
||||
}
|
||||
const { fetchPlayerRegistrations, isRecentPlayerRegistration } = require('#src/data/player-registry');
|
||||
|
||||
function normalizePlayerMetadata(player) {
|
||||
const playerIdentifier = String(player && player.identifier || '').trim();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Helpers for normalizing dashboard state and date values.
|
||||
|
||||
const { WebSocket } = require('ws');
|
||||
const { isRecentPlayerRegistration } = require('#src/data/player-registry');
|
||||
|
||||
function normalizeClientName(value) {
|
||||
return String(value || '').trim();
|
||||
@@ -52,14 +53,8 @@ function buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboa
|
||||
});
|
||||
}
|
||||
|
||||
function isRecentPlayerRegistration(player, staleSeconds) {
|
||||
const lastSeenAt = player && player.last_seen_at;
|
||||
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
|
||||
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
|
||||
|
||||
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
|
||||
}
|
||||
|
||||
// Only build launcher entries for recent registrations with a public URL. This keeps the kiosk
|
||||
// dashboard list focused on active devices and avoids presenting stale players as launch targets.
|
||||
function buildKioskLauncherPlayers(playerRegistrations, staleSeconds) {
|
||||
return (Array.isArray(playerRegistrations) ? playerRegistrations : [])
|
||||
.filter(function (player) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Media helper entry point for upload sync and thumbnail capture.
|
||||
|
||||
module.exports = {
|
||||
createUploadSyncService: require('./upload-sync').createUploadSyncService,
|
||||
createUploadSyncService: require('#src/web/lib/media/upload-sync').createUploadSyncService,
|
||||
captureSlideThumbnail: function captureSlideThumbnail(options) {
|
||||
return require('./slide-thumbnails').captureSlideThumbnail(options);
|
||||
return require('#src/web/lib/media/slide-thumbnails').captureSlideThumbnail(options);
|
||||
},
|
||||
fontLibrary: require('./font-library')
|
||||
fontLibrary: require('#src/web/lib/media/font-library')
|
||||
};
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const sharp = require('sharp');
|
||||
|
||||
const MEDIA_MIME_TYPES = {
|
||||
'.gif': 'image/gif',
|
||||
@@ -17,6 +19,121 @@ const MEDIA_MIME_TYPES = {
|
||||
'.webm': 'video/webm'
|
||||
};
|
||||
|
||||
const OPTIMIZABLE_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
|
||||
const IMAGE_VARIANT_DIMENSIONS = [2048, 4096, 8192, 16384];
|
||||
|
||||
function getImageVariantPath(filePath, dimension) {
|
||||
const extension = path.extname(filePath);
|
||||
const baseName = path.basename(filePath, extension);
|
||||
return path.join(path.dirname(filePath), baseName + '.variant-' + dimension + extension);
|
||||
}
|
||||
|
||||
async function removeImageVariants(filePath) {
|
||||
const normalizedPath = String(filePath || '').trim();
|
||||
const extension = path.extname(normalizedPath).toLowerCase();
|
||||
if (!normalizedPath || !OPTIMIZABLE_IMAGE_EXTENSIONS.has(extension)) {
|
||||
return;
|
||||
}
|
||||
await Promise.all(IMAGE_VARIANT_DIMENSIONS.map(function (dimension) {
|
||||
return fs.promises.unlink(getImageVariantPath(normalizedPath, dimension)).catch(function (error) {
|
||||
if (error && error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async function createImageVariants(file) {
|
||||
const filePath = String(file && file.path || '').trim();
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
if (!filePath || !OPTIMIZABLE_IMAGE_EXTENSIONS.has(extension)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const maxDimension = Math.max.apply(null, IMAGE_VARIANT_DIMENSIONS);
|
||||
const originalStats = await fs.promises.stat(filePath);
|
||||
const metadata = await sharp(filePath, { failOn: 'none' }).metadata();
|
||||
const sourceDimension = Math.max(Number(metadata.width) || 0, Number(metadata.height) || 0);
|
||||
const variants = [];
|
||||
|
||||
for (const dimension of IMAGE_VARIANT_DIMENSIONS) {
|
||||
if (dimension > maxDimension || dimension >= sourceDimension) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const transformer = sharp(filePath, { failOn: 'none' })
|
||||
.rotate()
|
||||
.resize({ width: dimension, height: dimension, fit: 'inside', withoutEnlargement: true });
|
||||
if (extension === '.jpg' || extension === '.jpeg') {
|
||||
transformer.jpeg({ quality: 82, progressive: true, mozjpeg: true });
|
||||
} else if (extension === '.png') {
|
||||
transformer.png({ compressionLevel: 9, adaptiveFiltering: true });
|
||||
} else {
|
||||
transformer.webp({ quality: 82, effort: 5 });
|
||||
}
|
||||
|
||||
const variantBuffer = await transformer.toBuffer();
|
||||
if (!variantBuffer.length || variantBuffer.length >= originalStats.size) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const variantPath = getImageVariantPath(filePath, dimension);
|
||||
const temporaryPath = variantPath + '.partial-' + crypto.randomUUID();
|
||||
try {
|
||||
await fs.promises.writeFile(temporaryPath, variantBuffer);
|
||||
await fs.promises.rename(temporaryPath, variantPath);
|
||||
variants.push({
|
||||
width: dimension,
|
||||
path: '/media/uploads/' + path.basename(variantPath),
|
||||
fileSize: variantBuffer.length
|
||||
});
|
||||
} finally {
|
||||
await fs.promises.unlink(temporaryPath).catch(function () {});
|
||||
}
|
||||
}
|
||||
|
||||
return variants;
|
||||
}
|
||||
|
||||
async function optimizeUploadedImage(file, options) {
|
||||
const filePath = String(file && file.path || '').trim();
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
if (!filePath || !OPTIMIZABLE_IMAGE_EXTENSIONS.has(extension)) {
|
||||
return file;
|
||||
}
|
||||
|
||||
const requestedMaxDimension = Number(options && options.maxDimension);
|
||||
const maxDimension = Number.isInteger(requestedMaxDimension) && requestedMaxDimension > 0
|
||||
? requestedMaxDimension
|
||||
: 4096;
|
||||
const originalStats = await fs.promises.stat(filePath);
|
||||
const transformer = sharp(filePath, { failOn: 'none' })
|
||||
.rotate()
|
||||
.resize({ width: maxDimension, height: maxDimension, fit: 'inside', withoutEnlargement: true });
|
||||
if (extension === '.jpg' || extension === '.jpeg') {
|
||||
transformer.jpeg({ quality: 82, progressive: true, mozjpeg: true });
|
||||
} else if (extension === '.png') {
|
||||
transformer.png({ compressionLevel: 9, adaptiveFiltering: true });
|
||||
} else {
|
||||
transformer.webp({ quality: 82, effort: 5 });
|
||||
}
|
||||
|
||||
const optimizedBuffer = await transformer.toBuffer();
|
||||
if (!optimizedBuffer.length || optimizedBuffer.length >= originalStats.size) {
|
||||
return file;
|
||||
}
|
||||
|
||||
const temporaryPath = filePath + '.optimized-' + crypto.randomUUID();
|
||||
try {
|
||||
await fs.promises.writeFile(temporaryPath, optimizedBuffer);
|
||||
await fs.promises.rename(temporaryPath, filePath);
|
||||
file.size = optimizedBuffer.length;
|
||||
return file;
|
||||
} finally {
|
||||
await fs.promises.unlink(temporaryPath).catch(function () {});
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMediaPath(value) {
|
||||
const mediaPath = String(value || '').trim();
|
||||
return mediaPath.startsWith('/media/') ? mediaPath : null;
|
||||
@@ -103,6 +220,9 @@ async function syncMediaAssetsFromDirectory(pool, uploadDir, userId) {
|
||||
continue;
|
||||
}
|
||||
const originalName = String(entry.name || '').trim();
|
||||
if (/\.variant-\d+\.(?:jpe?g|png|webp)$/i.test(originalName)) {
|
||||
continue;
|
||||
}
|
||||
const mimeType = MEDIA_MIME_TYPES[path.extname(originalName).toLowerCase()];
|
||||
if (!mimeType) {
|
||||
continue;
|
||||
@@ -163,11 +283,15 @@ async function removePendingMediaAssets(pool, uploadDir, mediaPaths) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
await removeImageVariants(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeMediaPath,
|
||||
createImageVariants,
|
||||
removeImageVariants,
|
||||
optimizeUploadedImage,
|
||||
registerMediaAsset,
|
||||
registerMediaAssets,
|
||||
fetchMediaAssets,
|
||||
|
||||
@@ -156,6 +156,10 @@ function buildRegionInnerHtml(region, regionContent, baseUrl) {
|
||||
return '<div class="slide-preview-region slide-preview-rtmp-region" style="' + region.baseStyle + '"><div class="slide-preview-rtmp-placeholder">' + escapeHtml(label) + '</div></div>';
|
||||
}
|
||||
|
||||
if (regionType === 'chart') {
|
||||
return '<div class="slide-preview-region slide-preview-chart-region" style="' + region.baseStyle + 'position:relative;background:transparent;"><canvas data-chart-json="' + escapeHtml(JSON.stringify(regionContent || {})) + '" aria-label="' + escapeHtml(region.label || region.region_key || 'chart') + '"></canvas></div>';
|
||||
}
|
||||
|
||||
return buildTextRegionMarkup(region, regionContent);
|
||||
}
|
||||
|
||||
@@ -188,6 +192,7 @@ function buildThumbnailPreviewPayload(slide, options) {
|
||||
backgroundColor: template && template.background_color ? String(template.background_color) : '#111111',
|
||||
backgroundGradient: template && template.background_gradient ? String(template.background_gradient) : '',
|
||||
backgroundImagePath: template && template.background_image_path ? String(template.background_image_path) : '',
|
||||
apiSources: Array.isArray(options && options.apiSources) ? options.apiSources : [],
|
||||
fontStylesheetHref: String(options && options.fontStylesheetHref || '').trim(),
|
||||
html: buildThumbnailPreviewMarkup(slide, options && options.baseUrl)
|
||||
};
|
||||
|
||||
@@ -1,53 +1,29 @@
|
||||
// Upload sync helpers for mirroring web media to player storage.
|
||||
// This service owns the upload queue and deferred player-target selection, while the shared
|
||||
// player registry helper owns the repeated URL/heartbeat resolution rules used elsewhere.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const multer = require('multer');
|
||||
const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const { collectFontLibrarySyncOperations } = require('./font-library');
|
||||
const { fetchPlayerRegistrations, getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
|
||||
const { collectFontLibrarySyncOperations } = require('#src/web/lib/media/font-library');
|
||||
const {
|
||||
fetchPlayerRegistrations,
|
||||
getConfiguredPlayerIdentifier,
|
||||
isLocalLikeBaseUrl,
|
||||
isRecentPlayerRegistration,
|
||||
normalizeBaseUrl
|
||||
} = require('#src/data/player-registry');
|
||||
|
||||
function normalizeUploadRoot(uploadDir) {
|
||||
return path.resolve(String(uploadDir || '').trim());
|
||||
}
|
||||
|
||||
function isLocalLikeBaseUrl(value) {
|
||||
let host = '';
|
||||
try {
|
||||
host = new URL(String(value || '').trim().replace(/\/$/, '')).hostname.toLowerCase();
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return host === 'localhost'
|
||||
|| host === '127.0.0.1'
|
||||
|| host === '::1'
|
||||
|| host === 'host.docker.internal'
|
||||
|| host === 'player'
|
||||
|| host === 'web'
|
||||
|| host === 'player-bridge'
|
||||
|| host.endsWith('.local')
|
||||
|| host.endsWith('.internal')
|
||||
|| host.endsWith('.docker.internal');
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function normalizePlayerRowBaseUrl(player) {
|
||||
return normalizeBaseUrl(player && player.internal_base_url);
|
||||
}
|
||||
|
||||
function isRecentPlayerRegistration(player, staleSeconds) {
|
||||
const lastSeenAt = player && player.last_seen_at;
|
||||
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
|
||||
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
|
||||
|
||||
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
|
||||
}
|
||||
|
||||
function createUploadSyncService(options) {
|
||||
const pool = options && options.pool;
|
||||
const common = options && options.common;
|
||||
@@ -297,6 +273,30 @@ function createUploadSyncService(options) {
|
||||
return path.join(mediaRoot, relativePath);
|
||||
}
|
||||
|
||||
async function getUploadPathsWithVariants(uploadPath, uploadDir) {
|
||||
const normalizedPath = normalizeUploadReference(uploadPath);
|
||||
const sourcePath = resolveUploadFilePath(uploadDir, normalizedPath);
|
||||
if (!normalizedPath || !sourcePath || !/\.(?:jpe?g|png|webp)$/i.test(normalizedPath)) {
|
||||
return normalizedPath ? [normalizedPath] : [];
|
||||
}
|
||||
|
||||
const extension = path.extname(sourcePath);
|
||||
const baseName = path.basename(sourcePath, extension);
|
||||
const directory = path.dirname(sourcePath);
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
||||
} catch (_error) {
|
||||
return [normalizedPath];
|
||||
}
|
||||
|
||||
const variantPattern = new RegExp('^' + baseName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&') + '\\.variant-\\d+' + extension.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&') + '$', 'i');
|
||||
return [normalizedPath].concat(entries
|
||||
.filter(function (entry) { return entry && entry.isFile() && variantPattern.test(entry.name); })
|
||||
.map(function (entry) { return normalizedPath.replace(/[^/]+$/, entry.name); })
|
||||
.sort());
|
||||
}
|
||||
|
||||
function collectUploadReferencesFromValue(value, refs) {
|
||||
if (!value) {
|
||||
return refs;
|
||||
@@ -584,8 +584,8 @@ function createUploadSyncService(options) {
|
||||
}
|
||||
}
|
||||
|
||||
async function removeUploadFileFromPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier) {
|
||||
if (!uploadPath || !shouldMirrorUploads(localUploadDir)) {
|
||||
async function removeSingleUploadFileFromPlayer(uploadPath, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier) {
|
||||
if (!uploadPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -632,6 +632,17 @@ function createUploadSyncService(options) {
|
||||
}
|
||||
}
|
||||
|
||||
async function removeUploadFileFromPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier) {
|
||||
const uploadPaths = await getUploadPathsWithVariants(uploadPath, localUploadDir);
|
||||
let success = true;
|
||||
for (let index = 0; index < uploadPaths.length; index += 1) {
|
||||
if (!await removeSingleUploadFileFromPlayer(uploadPaths[index], resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
async function syncUploadRefsToPlayer(uploadRefs, localUploadDir, preferredPlayerIdentifier, preferredPlayerInternalBaseUrl) {
|
||||
if (!shouldMirrorUploads(localUploadDir)) {
|
||||
return;
|
||||
@@ -640,19 +651,23 @@ function createUploadSyncService(options) {
|
||||
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl(preferredPlayerIdentifier, preferredPlayerInternalBaseUrl);
|
||||
const uniqueRefs = Array.from(new Set((uploadRefs || []).map(normalizeUploadReference).filter(Boolean)));
|
||||
for (let i = 0; i < uniqueRefs.length; i += 1) {
|
||||
const success = await pushUploadFileToPlayer(uniqueRefs[i], localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier);
|
||||
if (!success) {
|
||||
queuePlayerUploadSync({
|
||||
type: 'put',
|
||||
uploadPath: uniqueRefs[i],
|
||||
uploadDir: localUploadDir,
|
||||
playerIdentifier: preferredPlayerIdentifier,
|
||||
playerInternalBaseUrl: preferredPlayerInternalBaseUrl,
|
||||
metadata: preferredPlayerIdentifier || preferredPlayerInternalBaseUrl ? {
|
||||
playerIdentifier: preferredPlayerIdentifier || null,
|
||||
playerInternalBaseUrl: preferredPlayerInternalBaseUrl || null
|
||||
} : null
|
||||
});
|
||||
const uploadPaths = await getUploadPathsWithVariants(uniqueRefs[i], localUploadDir);
|
||||
for (let pathIndex = 0; pathIndex < uploadPaths.length; pathIndex += 1) {
|
||||
const uploadPath = uploadPaths[pathIndex];
|
||||
const success = await pushUploadFileToPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier);
|
||||
if (!success) {
|
||||
queuePlayerUploadSync({
|
||||
type: 'put',
|
||||
uploadPath: uploadPath,
|
||||
uploadDir: localUploadDir,
|
||||
playerIdentifier: preferredPlayerIdentifier,
|
||||
playerInternalBaseUrl: preferredPlayerInternalBaseUrl,
|
||||
metadata: preferredPlayerIdentifier || preferredPlayerInternalBaseUrl ? {
|
||||
playerIdentifier: preferredPlayerIdentifier || null,
|
||||
playerInternalBaseUrl: preferredPlayerInternalBaseUrl || null
|
||||
} : null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -938,14 +953,17 @@ function createUploadSyncService(options) {
|
||||
}).forEach(function (uploadPath) {
|
||||
uploadRefs.add(uploadPath);
|
||||
});
|
||||
Array.from(uploadRefs).forEach(function (uploadPath) {
|
||||
queuePlayerUploadSync({
|
||||
type: 'put',
|
||||
uploadPath: uploadPath,
|
||||
uploadDir: uploadDir,
|
||||
metadata: playerMetadata
|
||||
for (const uploadPath of uploadRefs) {
|
||||
const uploadPaths = await getUploadPathsWithVariants(uploadPath, uploadDir);
|
||||
uploadPaths.forEach(function (variantPath) {
|
||||
queuePlayerUploadSync({
|
||||
type: 'put',
|
||||
uploadPath: variantPath,
|
||||
uploadDir: uploadDir,
|
||||
metadata: playerMetadata
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
fontLibraryOperations.forEach(function (operation) {
|
||||
if (!operation || !operation.uploadPath) {
|
||||
return;
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
// Commands and synchronization actions forwarded from the web app to players.
|
||||
// Keep the routing layer separate from the actual player device commands so the web app can
|
||||
// resolve a target player URL without mixing in connection or device-specific logic.
|
||||
|
||||
const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const { getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
|
||||
|
||||
function isLocalLikeBaseUrl(value) {
|
||||
let host = '';
|
||||
try {
|
||||
host = new URL(String(value || '').trim().replace(/\/$/, '')).hostname.toLowerCase();
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return host === 'localhost'
|
||||
|| host === '127.0.0.1'
|
||||
|| host === '::1'
|
||||
|| host === 'host.docker.internal'
|
||||
|| host === 'player'
|
||||
|| host === 'web'
|
||||
|| host === 'player-bridge'
|
||||
|| host.endsWith('.local')
|
||||
|| host.endsWith('.internal')
|
||||
|| host.endsWith('.docker.internal');
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function isRecentPlayerRegistration(player, staleSeconds) {
|
||||
const lastSeenAt = player && player.last_seen_at;
|
||||
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
|
||||
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
|
||||
|
||||
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
|
||||
}
|
||||
const {
|
||||
getConfiguredPlayerIdentifier,
|
||||
isLocalLikeBaseUrl,
|
||||
isRecentPlayerRegistration,
|
||||
normalizeBaseUrl
|
||||
} = require('#src/data/player-registry');
|
||||
|
||||
function createPlayerActionService(options) {
|
||||
const pool = options && options.pool;
|
||||
|
||||
@@ -6,7 +6,7 @@ const path = require('path');
|
||||
const PUBLIC_JS_ROOT = path.join(__dirname, '..', 'public', 'js');
|
||||
const REGION_ROOT_DIR = path.join(PUBLIC_JS_ROOT, 'regions');
|
||||
const REGION_TYPE_DIR = path.join(REGION_ROOT_DIR, 'type');
|
||||
const REGION_CORE_SCRIPTS = ['js/shared/placeholder-utils.js', 'js/shared/placeholder-chips.js', 'js/shared/placeholder-info.js', 'js/shared/time-date-placeholders.js', 'js/regions/region-utils.js', 'js/regions/region-types.js'];
|
||||
const REGION_CORE_SCRIPTS = ['js/shared/placeholder-utils.js', 'js/shared/placeholder-chips.js', 'js/shared/placeholder-info.js', 'js/shared/time-date-placeholders.js', 'js/regions/region-utils.js', 'js/regions/region-types.js', 'js/regions/chart-data.js', 'vendor/chart.js/chart.umd.js'];
|
||||
|
||||
function withAssetVersion(scriptPath, assetVersion) {
|
||||
if (!assetVersion) {
|
||||
|
||||
@@ -58,7 +58,7 @@ module.exports = {
|
||||
renderMediaLibraryPage: require(routePath('settings', 'media-library', 'list')),
|
||||
renderBackgroundTasksPage: require(routePath('settings', 'background-tasks-page')).renderBackgroundTasksPage,
|
||||
renderBackgroundTasksScheduledPage: require(routePath('settings', 'background-tasks-page')).renderBackgroundTasksScheduledPage,
|
||||
renderErrorPage: require('./error'),
|
||||
renderErrorPage: require('#src/web/pages/error'),
|
||||
renderRbacPage: require(routePath('settings', 'rbac', 'list')),
|
||||
renderRbacAddPage: require(routePath('settings', 'rbac', 'add')),
|
||||
renderRbacEditPage: require(routePath('settings', 'rbac', 'edit'))
|
||||
|
||||
@@ -162,12 +162,6 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform .2s ease;
|
||||
}
|
||||
|
||||
.media-gallery-preview:hover img,
|
||||
.media-gallery-preview:hover video {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.media-gallery-preview:focus-visible {
|
||||
|
||||
@@ -204,6 +204,87 @@
|
||||
}
|
||||
}
|
||||
|
||||
function queueUploadCleanup(input, uploadPath) {
|
||||
var path = String(uploadPath || '').trim();
|
||||
var cleanupUrl = String(input && input.dataset.uploadCleanupUrl || '').trim();
|
||||
if (!path || !cleanupUrl) {
|
||||
return;
|
||||
}
|
||||
var payload = JSON.stringify({ uploadPaths: [path] });
|
||||
if (navigator.sendBeacon) {
|
||||
try {
|
||||
navigator.sendBeacon(cleanupUrl, new Blob([payload], { type: 'application/json' }));
|
||||
return;
|
||||
} catch (_error) {
|
||||
// Fall through to fetch when sendBeacon is unavailable.
|
||||
}
|
||||
}
|
||||
fetch(cleanupUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: payload,
|
||||
credentials: 'same-origin',
|
||||
keepalive: true
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
async function uploadSelectedFile(input, file) {
|
||||
var uploadUrl = String(input.dataset.uploadUrl || '').trim();
|
||||
var hidden = document.getElementById(String(input.dataset.uploadHidden || '').trim());
|
||||
if (!uploadUrl || !hidden || !file) {
|
||||
return;
|
||||
}
|
||||
|
||||
var maxBytes = Number(input.dataset.imageMaxBytes || 0);
|
||||
if (maxBytes > 0 && file.size > maxBytes) {
|
||||
input.value = '';
|
||||
window.PulseUpload.showMessage('This file is larger than the current upload limit of ' + window.PulseUpload.formatUploadLimit(maxBytes) + '.');
|
||||
return;
|
||||
}
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('file', file, file.name || 'upload');
|
||||
input.setCustomValidity('Upload is still in progress.');
|
||||
try {
|
||||
var response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin',
|
||||
headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
var payload = await response.json().catch(function () { return {}; });
|
||||
if (!response.ok || !payload.path) {
|
||||
throw new Error(payload.error || 'Unable to upload media.');
|
||||
}
|
||||
var previousPath = String(input.dataset.uploadedPath || '').trim();
|
||||
if (previousPath && previousPath !== payload.path) {
|
||||
queueUploadCleanup(input, previousPath);
|
||||
}
|
||||
input.dataset.uploadedPath = payload.path;
|
||||
input.dataset.uploadedNeedsCleanup = '1';
|
||||
input.value = '';
|
||||
input.setCustomValidity('');
|
||||
hidden.value = payload.path;
|
||||
hidden.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
updatePreview(hidden, payload.path, 'image');
|
||||
} catch (error) {
|
||||
input.value = '';
|
||||
var currentPath = String(hidden.value || '').trim();
|
||||
var backgroundPreview = document.getElementById('background-preview');
|
||||
var backgroundEmpty = document.getElementById('background-empty');
|
||||
if (currentPath) {
|
||||
updatePreview(hidden, currentPath, 'image');
|
||||
} else if (backgroundPreview) {
|
||||
backgroundPreview.removeAttribute('src');
|
||||
backgroundPreview.style.display = 'none';
|
||||
if (backgroundEmpty) backgroundEmpty.style.display = 'flex';
|
||||
}
|
||||
input.setCustomValidity('');
|
||||
window.PulseUpload.showMessage(error.message || 'Unable to upload media.');
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var uploadTrigger = event.target && event.target.closest ? event.target.closest('[data-media-picker-upload]') : null;
|
||||
if (uploadTrigger) {
|
||||
@@ -247,8 +328,46 @@
|
||||
updateSelection();
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var removeBackground = event.target && event.target.closest ? event.target.closest('#remove-background-image') : null;
|
||||
if (!removeBackground) {
|
||||
return;
|
||||
}
|
||||
var input = document.getElementById('background-image');
|
||||
if (input && input.dataset.uploadedNeedsCleanup === '1') {
|
||||
queueUploadCleanup(input, input.dataset.uploadedPath);
|
||||
delete input.dataset.uploadedNeedsCleanup;
|
||||
delete input.dataset.uploadedPath;
|
||||
}
|
||||
});
|
||||
|
||||
if (search) search.addEventListener('input', resetVisibleItemLimit);
|
||||
if (sort) sort.addEventListener('change', resetVisibleItemLimit);
|
||||
document.addEventListener('change', function (event) {
|
||||
var input = event.target;
|
||||
if (!input || input.type !== 'file' || !input.dataset) {
|
||||
return;
|
||||
}
|
||||
var file = input.files && input.files[0];
|
||||
if (input.dataset.uploadUrl) {
|
||||
input.setCustomValidity('');
|
||||
if (file) {
|
||||
uploadSelectedFile(input, file);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!input.dataset.uploadMaxBytes) {
|
||||
return;
|
||||
}
|
||||
var maxBytes = Number(input.dataset.uploadMaxBytes);
|
||||
if (!file || !Number.isFinite(maxBytes) || maxBytes <= 0 || file.size <= maxBytes) {
|
||||
input.setCustomValidity('');
|
||||
return;
|
||||
}
|
||||
input.value = '';
|
||||
input.setCustomValidity('Image must be ' + Math.round(maxBytes / 1024 / 1024) + ' MB or smaller.');
|
||||
input.reportValidity();
|
||||
});
|
||||
if (loadMoreButton) loadMoreButton.addEventListener('click', async function () {
|
||||
loadMoreButton.disabled = true;
|
||||
try {
|
||||
@@ -274,12 +393,31 @@
|
||||
closePicker();
|
||||
return;
|
||||
}
|
||||
var input = getActiveFileInput();
|
||||
if (input && input.dataset.uploadedNeedsCleanup === '1' && input.dataset.uploadedPath !== pendingPath) {
|
||||
queueUploadCleanup(input, input.dataset.uploadedPath);
|
||||
delete input.dataset.uploadedNeedsCleanup;
|
||||
delete input.dataset.uploadedPath;
|
||||
}
|
||||
hidden.value = pendingPath;
|
||||
updatePreview(hidden, pendingPath, pendingType);
|
||||
hidden.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
closePicker();
|
||||
});
|
||||
if (cancelButton) cancelButton.addEventListener('click', closePicker);
|
||||
document.querySelectorAll('input[type="file"][data-upload-url]').forEach(function (input) {
|
||||
var form = input.form;
|
||||
if (form) {
|
||||
form.addEventListener('submit', function () {
|
||||
delete input.dataset.uploadedNeedsCleanup;
|
||||
});
|
||||
}
|
||||
});
|
||||
window.addEventListener('beforeunload', function () {
|
||||
document.querySelectorAll('input[type="file"][data-upload-url][data-uploaded-needs-cleanup="1"]').forEach(function (input) {
|
||||
queueUploadCleanup(input, input.dataset.uploadedPath);
|
||||
});
|
||||
});
|
||||
modal.addEventListener('hidden.bs.modal', function () {
|
||||
activeButton = null;
|
||||
currentPath = '';
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
// Normalize chart configurations and source snapshots for editor previews and player playback.
|
||||
|
||||
(function () {
|
||||
var root = window;
|
||||
var allowedTypes = ['line', 'bar', 'doughnut', 'pie'];
|
||||
|
||||
function getPath(value, path) {
|
||||
var current = value;
|
||||
var parts = String(path || '').trim().replace(/\[([0-9]+)\]/g, '.$1').split('.').filter(Boolean);
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
if (current === undefined || current === null) {
|
||||
return undefined;
|
||||
}
|
||||
current = current[parts[index]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function toNumber(value) {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
var normalized = String(value === undefined || value === null ? '' : value).replace(/,/g, '').trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
var parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function normalizeChartType(value) {
|
||||
var type = String(value || 'bar').trim().toLowerCase();
|
||||
return allowedTypes.indexOf(type) !== -1 ? type : 'bar';
|
||||
}
|
||||
|
||||
function getApiItems(response, itemsPath) {
|
||||
var selected = itemsPath ? getPath(response, itemsPath) : response;
|
||||
if (Array.isArray(selected)) {
|
||||
return selected;
|
||||
}
|
||||
if (Array.isArray(response)) {
|
||||
return response;
|
||||
}
|
||||
if (response && Array.isArray(response.items)) return response.items;
|
||||
if (response && Array.isArray(response.results)) return response.results;
|
||||
if (response && Array.isArray(response.data)) return response.data;
|
||||
return response && typeof response === 'object' ? [response] : [];
|
||||
}
|
||||
|
||||
function normalizeRows(rows, mapping) {
|
||||
var labels = [];
|
||||
var values = [];
|
||||
var labelPath = String(mapping && (mapping.label || mapping.label_path) || 'label').trim();
|
||||
var valuePath = String(mapping && (mapping.value || mapping.value_path) || 'value').trim();
|
||||
(Array.isArray(rows) ? rows : []).forEach(function (row, index) {
|
||||
var label = getPath(row, labelPath);
|
||||
var value = toNumber(getPath(row, valuePath));
|
||||
if (value === null) {
|
||||
return;
|
||||
}
|
||||
labels.push(label === undefined || label === null || label === '' ? String(index + 1) : String(label));
|
||||
values.push(value);
|
||||
});
|
||||
return { labels: labels, values: values };
|
||||
}
|
||||
|
||||
function normalizeConfig(content) {
|
||||
var current = content && typeof content === 'object' ? content : {};
|
||||
var mapping = current.mapping && typeof current.mapping === 'object' ? current.mapping : {};
|
||||
var rawDatasets = Array.isArray(current.datasets) ? current.datasets : [];
|
||||
var datasets = rawDatasets.slice(0, 12).map(function (dataset, index) {
|
||||
var item = dataset && typeof dataset === 'object' ? dataset : {};
|
||||
return {
|
||||
label: String(item.label || 'Series ' + (index + 1)).trim() || 'Series ' + (index + 1),
|
||||
value_path: String(item.value_path || item.value || '').trim(),
|
||||
color: /^#[0-9a-fA-F]{3,8}$/.test(String(item.color || '').trim()) ? String(item.color).trim() : ''
|
||||
};
|
||||
}).filter(function (dataset) { return dataset.value_path; });
|
||||
var palette = Array.isArray(current.colors)
|
||||
? current.colors
|
||||
: String(current.color_palette || '').split(',');
|
||||
palette = palette.map(function (color) {
|
||||
return String(color || '').trim();
|
||||
}).filter(function (color) {
|
||||
return /^#[0-9a-fA-F]{3,8}$/.test(color);
|
||||
}).slice(0, 24);
|
||||
var fallbackColor = /^#[0-9a-fA-F]{3,8}$/.test(String(current.color || '').trim()) ? String(current.color).trim() : '#36a269';
|
||||
var chartOptions = current.chart_options && typeof current.chart_options === 'object' ? current.chart_options : {};
|
||||
var sourceOptions = current.source_options && typeof current.source_options === 'object' ? current.source_options : {};
|
||||
return {
|
||||
type: normalizeChartType(current.chart_type || current.chartType),
|
||||
source_kind: String(current.source_kind || current.sourceKind || 'manual').trim().toLowerCase() === 'api' ? 'api' : 'manual',
|
||||
source_id: current.source_id === undefined || current.source_id === '' ? '' : Number(current.source_id),
|
||||
items_path: String(current.items_path || '').trim(),
|
||||
label_path: String(current.label_path || mapping.label || '').trim(),
|
||||
value_path: String(current.value_path || mapping.value || '').trim(),
|
||||
dataset_label: String(current.dataset_label || 'Value').trim() || 'Value',
|
||||
color: fallbackColor,
|
||||
datasets: datasets,
|
||||
colors: palette.length ? palette : [fallbackColor, '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#14b8a6'],
|
||||
manual_rows: Array.isArray(current.manual_rows) ? current.manual_rows : [],
|
||||
chart_options: {
|
||||
legend: ['automatic', 'show', 'hide'].indexOf(String(chartOptions.legend || 'automatic')) !== -1 ? String(chartOptions.legend || 'automatic') : 'automatic',
|
||||
begin_at_zero: chartOptions.begin_at_zero !== false,
|
||||
y_axis_min: chartOptions.y_axis_min === undefined || chartOptions.y_axis_min === '' ? '' : Number(chartOptions.y_axis_min),
|
||||
y_axis_max: chartOptions.y_axis_max === undefined || chartOptions.y_axis_max === '' ? '' : Number(chartOptions.y_axis_max),
|
||||
line_points: ['automatic', 'show', 'hide'].indexOf(String(chartOptions.line_points || 'automatic')) !== -1 ? String(chartOptions.line_points || 'automatic') : 'automatic',
|
||||
line_fill: chartOptions.line_fill === true,
|
||||
bar_orientation: chartOptions.bar_orientation === 'horizontal' ? 'horizontal' : 'vertical',
|
||||
value_format: ['number', 'percent', 'currency'].indexOf(String(chartOptions.value_format || 'number')) !== -1 ? String(chartOptions.value_format || 'number') : 'number',
|
||||
decimal_places: Math.max(0, Math.min(4, Number(chartOptions.decimal_places) || 0)),
|
||||
text_size: Math.max(8, Math.min(48, Number(chartOptions.text_size) || 12)),
|
||||
grid_color: /^#[0-9a-fA-F]{6}$/.test(String(chartOptions.grid_color || '').trim()) ? String(chartOptions.grid_color).trim() : '#000000',
|
||||
text_color: /^#[0-9a-fA-F]{6}$/.test(String(chartOptions.text_color || '').trim()) ? String(chartOptions.text_color).trim() : '#000000'
|
||||
},
|
||||
source_options: {
|
||||
sort: ['none', 'reverse-original', 'value-ascending', 'value-descending'].indexOf(String(sourceOptions.sort || 'none')) !== -1 ? String(sourceOptions.sort || 'none') : 'none',
|
||||
limit: Math.max(0, Math.min(500, Number(sourceOptions.limit) || 0)),
|
||||
missing: ['skip', 'zero', 'gap'].indexOf(String(sourceOptions.missing || 'gap')) !== -1 ? String(sourceOptions.missing || 'gap') : 'gap'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function isColor(value) {
|
||||
return /^#[0-9a-fA-F]{3,8}$/.test(String(value || '').trim());
|
||||
}
|
||||
|
||||
function getSeries(config) {
|
||||
if (config.type === 'line' && config.datasets.length) {
|
||||
return config.datasets;
|
||||
}
|
||||
var firstDataset = config.datasets[0] || {};
|
||||
return [{
|
||||
label: config.dataset_label,
|
||||
value_path: config.value_path || firstDataset.value_path || 'value',
|
||||
color: config.color || firstDataset.color
|
||||
}];
|
||||
}
|
||||
|
||||
function normalizeSeries(rows, config) {
|
||||
var series = getSeries(config);
|
||||
var labels = [];
|
||||
var values = series.map(function () { return []; });
|
||||
var colors = [];
|
||||
var sourceOptions = config.source_options || {};
|
||||
var sourceRows = (Array.isArray(rows) ? rows : []).slice();
|
||||
if (sourceOptions.sort !== 'none' && sourceOptions.sort !== 'reverse-original') {
|
||||
sourceRows.sort(function (left, right) {
|
||||
var firstSeries = series[0] || {};
|
||||
var leftValue = toNumber(getPath(left, firstSeries.value_path));
|
||||
var rightValue = toNumber(getPath(right, firstSeries.value_path));
|
||||
if (leftValue === null) return 1;
|
||||
if (rightValue === null) return -1;
|
||||
return (leftValue - rightValue) * (sourceOptions.sort === 'value-descending' ? -1 : 1);
|
||||
});
|
||||
}
|
||||
if (sourceOptions.sort === 'reverse-original') sourceRows.reverse();
|
||||
if (sourceOptions.limit > 0) sourceRows = sourceRows.slice(0, sourceOptions.limit);
|
||||
sourceRows.forEach(function (row, rowIndex) {
|
||||
var rowValues = series.map(function (dataset) {
|
||||
return toNumber(getPath(row, dataset.value_path));
|
||||
});
|
||||
if (!rowValues.some(function (value) { return value !== null; })) {
|
||||
return;
|
||||
}
|
||||
if (sourceOptions.missing === 'skip' && rowValues.some(function (value) { return value === null; })) {
|
||||
return;
|
||||
}
|
||||
if (sourceOptions.missing === 'zero') {
|
||||
rowValues = rowValues.map(function (value) { return value === null ? 0 : value; });
|
||||
}
|
||||
var label = getPath(row, config.label_path || 'label');
|
||||
labels.push(label === undefined || label === null || label === '' ? String(rowIndex + 1) : String(label));
|
||||
rowValues.forEach(function (value, datasetIndex) {
|
||||
values[datasetIndex].push(value);
|
||||
});
|
||||
colors.push(isColor(row && row.color) ? String(row.color).trim() : config.colors[rowIndex % config.colors.length]);
|
||||
});
|
||||
return { labels: labels, values: values, colors: colors };
|
||||
}
|
||||
|
||||
function fromContent(content, sources) {
|
||||
var config = normalizeConfig(content);
|
||||
var rows = config.manual_rows;
|
||||
if (config.source_kind === 'api') {
|
||||
var source = (Array.isArray(sources) ? sources : []).find(function (entry) {
|
||||
return Number(entry.id) === Number(config.source_id);
|
||||
});
|
||||
rows = getApiItems(source && source.responseJson, config.items_path || source && (source.items_path || source.itemsPath) || '');
|
||||
}
|
||||
var normalizedRows = normalizeSeries(rows, config);
|
||||
var series = getSeries(config);
|
||||
var linePointRadius = config.chart_options.line_points === 'show' ? 3 : config.chart_options.line_points === 'hide' ? 0 : undefined;
|
||||
return {
|
||||
type: config.type,
|
||||
options: { chart_options: config.chart_options },
|
||||
labels: normalizedRows.labels,
|
||||
datasets: series.map(function (dataset, index) {
|
||||
var color = dataset.color || config.colors[index % config.colors.length];
|
||||
var pointColors = config.type === 'bar' || config.type === 'pie' || config.type === 'doughnut' ? normalizedRows.colors : null;
|
||||
return {
|
||||
label: dataset.label,
|
||||
data: normalizedRows.values[index],
|
||||
backgroundColor: pointColors || color,
|
||||
borderColor: pointColors || color,
|
||||
borderWidth: 2,
|
||||
tension: 0.25,
|
||||
fill: config.type === 'line' && config.chart_options.line_fill,
|
||||
pointRadius: linePointRadius
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function getChartOptions(chartData) {
|
||||
var chartOptions = chartData.options && chartData.options.chart_options || {};
|
||||
var textSize = Math.max(8, Math.min(48, Number(chartOptions.text_size) || 12));
|
||||
var legendDefault = chartData.type !== 'bar' && (chartData.type !== 'line' || (chartData.datasets || []).length > 1);
|
||||
var legendDisplay = chartOptions.legend === 'show' ? true : chartOptions.legend === 'hide' ? false : legendDefault;
|
||||
var scales = {};
|
||||
var ticks = { color: chartOptions.text_color || '#000000', font: { size: textSize } };
|
||||
if (chartData.type !== 'pie' && chartData.type !== 'doughnut') {
|
||||
var valueScale = { beginAtZero: chartOptions.begin_at_zero !== false, grid: { color: chartOptions.grid_color || '#000000' }, ticks: ticks };
|
||||
if (chartOptions.y_axis_min !== '') valueScale.min = chartOptions.y_axis_min;
|
||||
if (chartOptions.y_axis_max !== '') valueScale.max = chartOptions.y_axis_max;
|
||||
scales[chartData.type === 'bar' && chartOptions.bar_orientation === 'horizontal' ? 'x' : 'y'] = valueScale;
|
||||
if (chartData.type === 'bar' && chartOptions.bar_orientation === 'horizontal') scales.y = { beginAtZero: true, grid: { color: chartOptions.grid_color || '#000000' }, ticks: ticks };
|
||||
if (!scales.x) scales.x = { grid: { color: chartOptions.grid_color || '#000000' }, ticks: ticks };
|
||||
if (!scales.y) scales.y = { grid: { color: chartOptions.grid_color || '#000000' }, ticks: ticks };
|
||||
}
|
||||
var tooltipLabel = function (context) {
|
||||
var value = context.parsed && (context.parsed.y === undefined ? context.parsed : context.parsed.y);
|
||||
var decimals = Number(chartOptions.decimal_places) || 0;
|
||||
if (chartOptions.value_format === 'percent') return Number(value).toFixed(decimals) + '%';
|
||||
if (chartOptions.value_format === 'currency') return '$' + Number(value).toFixed(decimals);
|
||||
return Number(value).toLocaleString(undefined, { maximumFractionDigits: decimals });
|
||||
};
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
indexAxis: chartData.type === 'bar' && chartOptions.bar_orientation === 'horizontal' ? 'y' : 'x',
|
||||
color: chartOptions.text_color || '#000000',
|
||||
font: { size: textSize },
|
||||
plugins: { legend: { display: legendDisplay, labels: { color: chartOptions.text_color || '#000000', font: { size: textSize } } }, tooltip: { titleColor: chartOptions.text_color || '#000000', bodyColor: chartOptions.text_color || '#000000', footerColor: chartOptions.text_color || '#000000', titleFont: { size: textSize }, bodyFont: { size: textSize }, footerFont: { size: textSize }, callbacks: { label: tooltipLabel } } },
|
||||
scales: scales
|
||||
};
|
||||
}
|
||||
|
||||
root.pulseChartData = {
|
||||
getPath: getPath,
|
||||
normalizeConfig: normalizeConfig,
|
||||
normalizeRows: normalizeRows,
|
||||
fromContent: fromContent,
|
||||
getChartOptions: getChartOptions
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,271 @@
|
||||
// Chart region editor and preview helpers.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var chartData = window.pulseChartData;
|
||||
var defaultSeriesColors = ['#36a269', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#14b8a6'];
|
||||
|
||||
function escapeHtml(value) {
|
||||
return window.pulseRegionUtils && window.pulseRegionUtils.escapeHtml
|
||||
? window.pulseRegionUtils.escapeHtml(value)
|
||||
: String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
function parseRows(value) {
|
||||
try {
|
||||
var parsed = JSON.parse(String(value || '[]'));
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentConfig(region, existingContent) {
|
||||
var current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||
var config = chartData.normalizeConfig(current);
|
||||
config.manual_rows_json = JSON.stringify(config.manual_rows, null, 2);
|
||||
config.datasets_json = JSON.stringify(config.datasets, null, 2);
|
||||
return config;
|
||||
}
|
||||
|
||||
function renderPreview(region, content, context) {
|
||||
var normalized = chartData.fromContent(content || {}, context && context.sources || []);
|
||||
var chartId = 'chart-preview-' + region.id;
|
||||
var label = escapeHtml((content && content.dataset_label) || 'Value');
|
||||
return '<div class="template-region chart" style="width:100%;height:100%;overflow:hidden;position:relative;">' +
|
||||
'<canvas id="' + chartId + '" data-chart-json="' + escapeHtml(JSON.stringify(normalized)) + '" aria-label="' + label + ' chart"></canvas>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function initPreview(root) {
|
||||
if (typeof window.Chart !== 'function' || !root || !root.querySelectorAll) {
|
||||
return;
|
||||
}
|
||||
root.querySelectorAll('.template-region.chart canvas').forEach(function (canvas) {
|
||||
var data = {};
|
||||
try { data = JSON.parse(canvas.getAttribute('data-chart-json') || '{}'); } catch (_error) { data = {}; }
|
||||
new window.Chart(canvas.getContext('2d'), {
|
||||
type: data.type || 'bar',
|
||||
data: { labels: data.labels || [], datasets: data.datasets || [] },
|
||||
options: chartData.getChartOptions(data)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncSourceFields(card) {
|
||||
if (!card || !card.querySelector) {
|
||||
return;
|
||||
}
|
||||
var sourceKind = card.querySelector('[name^="region_chart_source_kind_"]');
|
||||
var isApi = sourceKind && sourceKind.value === 'api';
|
||||
card.querySelectorAll('[data-chart-source-panel="api"], [data-chart-source-panel="manual"]').forEach(function (panel) {
|
||||
var visible = panel.getAttribute('data-chart-source-panel') === (isApi ? 'api' : 'manual');
|
||||
panel.hidden = !visible;
|
||||
panel.querySelectorAll('input, select, textarea').forEach(function (input) {
|
||||
input.disabled = !visible;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderDatasetRow(dataset, index, regionId) {
|
||||
var current = dataset || {};
|
||||
var hasColor = /^#[0-9a-fA-F]{3,8}$/.test(String(current.color || '').trim());
|
||||
return '<div class="row g-2 align-items-end mb-2" data-chart-dataset-row>' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_dataset_label_' + regionId + '_' + index + '">Series label</label><input id="region_chart_dataset_label_' + regionId + '_' + index + '" class="form-control" data-chart-dataset-label value="' + escapeHtml(current.label || 'Series ' + (index + 1)) + '" /></div>' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_dataset_value_' + regionId + '_' + index + '">Value field</label><input id="region_chart_dataset_value_' + regionId + '_' + index + '" class="form-control" data-chart-dataset-value value="' + escapeHtml(current.value_path || '') + '" placeholder="sales" /></div>' +
|
||||
'<div class="col-10 col-md-3"><label class="form-label" for="region_chart_dataset_color_' + regionId + '_' + index + '">Series color</label><input id="region_chart_dataset_color_' + regionId + '_' + index + '" class="form-control form-control-color w-100" data-chart-dataset-color data-chart-dataset-color-explicit="' + (hasColor ? 'true' : 'false') + '" type="color" value="' + escapeHtml(hasColor ? current.color : defaultSeriesColors[index % defaultSeriesColors.length]) + '" /></div>' +
|
||||
'<div class="col-2 col-md-1"><button type="button" class="btn btn-outline-danger w-100" data-chart-dataset-remove aria-label="Remove series" title="Remove series">×</button></div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function bindEditorCard(context) {
|
||||
var card = context && context.card;
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
var sourceKind = card.querySelector('[name^="region_chart_source_kind_"]');
|
||||
var chartType = card.querySelector('[name^="region_chart_type_"]');
|
||||
var datasetEditor = card.querySelector('[data-chart-dataset-editor]');
|
||||
var singleValueFields = card.querySelectorAll('[data-chart-single-value-field]');
|
||||
if (sourceKind) {
|
||||
sourceKind.addEventListener('change', function () {
|
||||
syncSourceFields(card);
|
||||
if (context.requestPreviewRender) context.requestPreviewRender();
|
||||
});
|
||||
}
|
||||
function syncDatasetEditor() {
|
||||
var usesDatasets = chartType && chartType.value === 'line';
|
||||
if (datasetEditor) {
|
||||
datasetEditor.hidden = !usesDatasets;
|
||||
}
|
||||
singleValueFields.forEach(function (field) {
|
||||
field.hidden = usesDatasets;
|
||||
});
|
||||
card.querySelectorAll('[data-chart-option-types]').forEach(function (field) {
|
||||
var types = String(field.getAttribute('data-chart-option-types') || '').split(' ');
|
||||
var hidden = !chartType || types.indexOf(chartType.value) === -1;
|
||||
field.hidden = hidden;
|
||||
field.classList.toggle('d-none', hidden);
|
||||
});
|
||||
}
|
||||
if (chartType) {
|
||||
chartType.addEventListener('change', function () {
|
||||
syncDatasetEditor();
|
||||
if (context.requestPreviewRender) context.requestPreviewRender();
|
||||
});
|
||||
}
|
||||
var regionId = card.getAttribute('data-region-id');
|
||||
var datasetList = card.querySelector('[data-chart-datasets]');
|
||||
var datasetInput = card.querySelector('[name="region_chart_datasets_' + regionId + '"]');
|
||||
var addDatasetButton = card.querySelector('[data-chart-dataset-add]');
|
||||
function syncDatasets() {
|
||||
var datasets = [];
|
||||
card.querySelectorAll('[data-chart-dataset-row]').forEach(function (row) {
|
||||
var label = row.querySelector('[data-chart-dataset-label]');
|
||||
var valuePath = row.querySelector('[data-chart-dataset-value]');
|
||||
var color = row.querySelector('[data-chart-dataset-color]');
|
||||
var explicitColor = color && color.getAttribute('data-chart-dataset-color-explicit') === 'true';
|
||||
if (valuePath && valuePath.value.trim()) {
|
||||
datasets.push({ label: label && label.value.trim() || 'Series ' + (datasets.length + 1), value_path: valuePath.value.trim(), color: explicitColor ? color.value : '' });
|
||||
}
|
||||
});
|
||||
if (datasetInput) datasetInput.value = JSON.stringify(datasets);
|
||||
if (context.requestPreviewRender) context.requestPreviewRender();
|
||||
}
|
||||
if (addDatasetButton && datasetList) {
|
||||
addDatasetButton.addEventListener('click', function () {
|
||||
var index = datasetList.querySelectorAll('[data-chart-dataset-row]').length;
|
||||
datasetList.insertAdjacentHTML('beforeend', renderDatasetRow({ label: 'Series ' + (index + 1), value_path: '' }, index, regionId));
|
||||
syncDatasets();
|
||||
});
|
||||
}
|
||||
if (datasetList) {
|
||||
datasetList.addEventListener('input', function (event) {
|
||||
if (event.target.matches('[data-chart-dataset-color]')) {
|
||||
event.target.setAttribute('data-chart-dataset-color-explicit', 'true');
|
||||
}
|
||||
syncDatasets();
|
||||
});
|
||||
datasetList.addEventListener('change', syncDatasets);
|
||||
datasetList.addEventListener('click', function (event) {
|
||||
var removeButton = event.target.closest('[data-chart-dataset-remove]');
|
||||
if (!removeButton) return;
|
||||
var rows = datasetList.querySelectorAll('[data-chart-dataset-row]');
|
||||
if (rows.length > 1) removeButton.closest('[data-chart-dataset-row]').remove();
|
||||
syncDatasets();
|
||||
});
|
||||
syncDatasets();
|
||||
}
|
||||
syncSourceFields(card);
|
||||
syncDatasetEditor();
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = context.current || {};
|
||||
var config = context.config || {};
|
||||
var chartOptions = config.chart_options || {};
|
||||
var sourceOptions = config.source_options || {};
|
||||
var datasets = config.datasets && config.datasets.length ? config.datasets : [{ label: config.dataset_label || 'Value', value_path: config.value_path || 'value', color: '' }];
|
||||
var datasetRows = datasets.map(function (dataset, index) { return renderDatasetRow(dataset, index, region.id); }).join('');
|
||||
var accordionId = 'region-chart-options-' + region.id;
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '" style="position:relative;overflow:visible !important;">' +
|
||||
'<div class="card-header template-field-head"><strong>' + escapeHtml(region.label) + '</strong><div class="template-field-actions"><span class="chip">Chart</span></div></div>' +
|
||||
'<div class="card-body p-3 d-grid gap-3">' +
|
||||
'<div class="row g-3">' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_type_' + region.id + '">Chart type</label><select id="region_chart_type_' + region.id + '" name="region_chart_type_' + region.id + '" class="form-select"><option value="bar"' + (config.type === 'bar' ? ' selected' : '') + '>Bar</option><option value="line"' + (config.type === 'line' ? ' selected' : '') + '>Line</option><option value="doughnut"' + (config.type === 'doughnut' ? ' selected' : '') + '>Doughnut</option><option value="pie"' + (config.type === 'pie' ? ' selected' : '') + '>Pie</option></select></div>' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_source_kind_' + region.id + '">Data source</label><select id="region_chart_source_kind_' + region.id + '" name="region_chart_source_kind_' + region.id + '" class="form-select"><option value="manual"' + (config.source_kind === 'manual' ? ' selected' : '') + '>Manual rows</option><option value="api"' + (config.source_kind === 'api' ? ' selected' : '') + '>API source</option></select></div>' +
|
||||
'<div class="col-12 col-md-4" data-chart-source-panel="api"><label class="form-label" for="region_chart_source_id_' + region.id + '">API source</label><select id="region_chart_source_id_' + region.id + '" name="region_chart_source_id_' + region.id + '" class="form-select"><option value="">Select a source</option>' + String(context.sourceOptions || '') + '</select></div>' +
|
||||
'</div>' +
|
||||
'<div class="row g-3" data-chart-source-panel="api">' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_items_path_' + region.id + '">Items path</label><input id="region_chart_items_path_' + region.id + '" name="region_chart_items_path_' + region.id + '" class="form-control" value="' + escapeHtml(config.items_path) + '" placeholder="results" /></div>' +
|
||||
'<div class="col-12 col-md-4" data-chart-single-value-field><label class="form-label" for="region_chart_label_path_' + region.id + '">Label field</label><input id="region_chart_label_path_' + region.id + '" name="region_chart_label_path_' + region.id + '" class="form-control" value="' + escapeHtml(config.label_path) + '" placeholder="date" /></div>' +
|
||||
'<div class="col-12 col-md-4" data-chart-single-value-field><label class="form-label" for="region_chart_value_path_' + region.id + '">Single value field</label><input id="region_chart_value_path_' + region.id + '" name="region_chart_value_path_' + region.id + '" class="form-control" value="' + escapeHtml(config.value_path) + '" placeholder="value" /></div>' +
|
||||
'</div>' +
|
||||
'<div class="row g-3">' +
|
||||
'<div class="col-12" data-chart-source-panel="manual"><label class="form-label" for="region_chart_manual_rows_' + region.id + '">Manual rows (JSON)</label><textarea id="region_chart_manual_rows_' + region.id + '" name="region_chart_manual_rows_' + region.id + '" class="form-control" rows="5">' + escapeHtml(config.manual_rows_json) + '</textarea><div class="form-text">Rows use a label and one or more mapped values, for example [{"label":"Mon","value":12,"color":"#36a269"}].</div></div>' +
|
||||
'<div class="col-12" data-chart-dataset-editor' + (config.type === 'line' ? '' : ' hidden') + '><div class="d-flex align-items-center justify-content-between gap-2"><label class="form-label mb-0">Data series</label><button type="button" class="btn btn-outline-secondary btn-sm" data-chart-dataset-add><i class="bi bi-plus-lg" aria-hidden="true"></i> Add series</button></div><div class="mt-2" data-chart-datasets>' + datasetRows + '</div><input type="hidden" name="region_chart_datasets_' + region.id + '" value="' + escapeHtml(JSON.stringify(datasets)) + '" /><div class="form-text">Add one section per API field or manual value. Set the color for each series here.</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="accordion" id="' + accordionId + '">' +
|
||||
'<div class="accordion-item overflow-hidden"><h2 class="accordion-header" id="' + accordionId + '-heading-chart"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#' + accordionId + '-chart" aria-expanded="false" aria-controls="' + accordionId + '-chart">Chart options</button></h2><div id="' + accordionId + '-chart" class="accordion-collapse collapse" aria-labelledby="' + accordionId + '-heading-chart" data-bs-parent="#' + accordionId + '"><div class="accordion-body"><div class="row g-3">' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_legend_' + region.id + '">Legend</label><select id="region_chart_legend_' + region.id + '" name="region_chart_legend_' + region.id + '" class="form-select"><option value="automatic"' + (chartOptions.legend === 'automatic' ? ' selected' : '') + '>Automatic</option><option value="show"' + (chartOptions.legend === 'show' ? ' selected' : '') + '>Show</option><option value="hide"' + (chartOptions.legend === 'hide' ? ' selected' : '') + '>Hide</option></select></div>' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_value_format_' + region.id + '">Value format</label><select id="region_chart_value_format_' + region.id + '" name="region_chart_value_format_' + region.id + '" class="form-select"><option value="number"' + (chartOptions.value_format === 'number' ? ' selected' : '') + '>Number</option><option value="percent"' + (chartOptions.value_format === 'percent' ? ' selected' : '') + '>Percent</option><option value="currency"' + (chartOptions.value_format === 'currency' ? ' selected' : '') + '>Currency</option></select></div>' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_decimal_places_' + region.id + '">Decimal places</label><input id="region_chart_decimal_places_' + region.id + '" name="region_chart_decimal_places_' + region.id + '" class="form-control" type="number" min="0" max="4" value="' + escapeHtml(chartOptions.decimal_places === undefined ? 0 : chartOptions.decimal_places) + '" /></div>' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_text_size_' + region.id + '">Text size (px)</label><input id="region_chart_text_size_' + region.id + '" name="region_chart_text_size_' + region.id + '" class="form-control" type="number" min="8" max="48" step="1" value="' + escapeHtml(chartOptions.text_size || 12) + '" /></div>' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_grid_color_' + region.id + '">Grid line color</label><input id="region_chart_grid_color_' + region.id + '" name="region_chart_grid_color_' + region.id + '" class="form-control form-control-color" type="color" value="' + escapeHtml(chartOptions.grid_color || '#000000') + '" title="Choose grid line color" /></div>' +
|
||||
'<div class="col-12 col-md-4"><label class="form-label" for="region_chart_text_color_' + region.id + '">Text color</label><input id="region_chart_text_color_' + region.id + '" name="region_chart_text_color_' + region.id + '" class="form-control form-control-color" type="color" value="' + escapeHtml(chartOptions.text_color || '#000000') + '" title="Choose chart text color" /></div>' +
|
||||
'<div class="col-12 col-md-4" data-chart-option-types="line bar"><label class="form-label" for="region_chart_y_axis_min_' + region.id + '">Y-axis minimum</label><input id="region_chart_y_axis_min_' + region.id + '" name="region_chart_y_axis_min_' + region.id + '" class="form-control" type="number" value="' + escapeHtml(chartOptions.y_axis_min) + '" placeholder="Automatic" /></div>' +
|
||||
'<div class="col-12 col-md-4" data-chart-option-types="line bar"><label class="form-label" for="region_chart_y_axis_max_' + region.id + '">Y-axis maximum</label><input id="region_chart_y_axis_max_' + region.id + '" name="region_chart_y_axis_max_' + region.id + '" class="form-control" type="number" value="' + escapeHtml(chartOptions.y_axis_max) + '" placeholder="Automatic" /></div>' +
|
||||
'<div class="col-12 col-md-4 d-flex align-items-end" data-chart-option-types="line bar"><div class="form-check mb-2"><input type="hidden" name="region_chart_begin_at_zero_control_' + region.id + '" value="1" /><input id="region_chart_begin_at_zero_' + region.id + '" name="region_chart_begin_at_zero_' + region.id + '" class="form-check-input" type="checkbox"' + (chartOptions.begin_at_zero !== false ? ' checked' : '') + ' /><label class="form-check-label" for="region_chart_begin_at_zero_' + region.id + '">Start Y-axis at zero</label></div></div>' +
|
||||
'<div class="col-12 col-md-4" data-chart-option-types="line"><label class="form-label" for="region_chart_line_points_' + region.id + '">Line points</label><select id="region_chart_line_points_' + region.id + '" name="region_chart_line_points_' + region.id + '" class="form-select"><option value="automatic"' + (chartOptions.line_points === 'automatic' ? ' selected' : '') + '>Automatic</option><option value="show"' + (chartOptions.line_points === 'show' ? ' selected' : '') + '>Show</option><option value="hide"' + (chartOptions.line_points === 'hide' ? ' selected' : '') + '>Hide</option></select></div>' +
|
||||
'<div class="col-12 col-md-4" data-chart-option-types="bar"><label class="form-label" for="region_chart_bar_orientation_' + region.id + '">Bar orientation</label><select id="region_chart_bar_orientation_' + region.id + '" name="region_chart_bar_orientation_' + region.id + '" class="form-select"><option value="vertical"' + (chartOptions.bar_orientation !== 'horizontal' ? ' selected' : '') + '>Vertical</option><option value="horizontal"' + (chartOptions.bar_orientation === 'horizontal' ? ' selected' : '') + '>Horizontal</option></select></div>' +
|
||||
'<div class="col-12 col-md-4 d-flex align-items-end" data-chart-option-types="line"><div class="form-check mb-2"><input id="region_chart_line_fill_' + region.id + '" name="region_chart_line_fill_' + region.id + '" class="form-check-input" type="checkbox"' + (chartOptions.line_fill ? ' checked' : '') + ' /><label class="form-check-label" for="region_chart_line_fill_' + region.id + '">Fill line area</label></div></div>' +
|
||||
'</div></div></div></div>' +
|
||||
'<div class="accordion-item overflow-hidden"><h2 class="accordion-header" id="' + accordionId + '-heading-source"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#' + accordionId + '-source" aria-expanded="false" aria-controls="' + accordionId + '-source">Source options</button></h2><div id="' + accordionId + '-source" class="accordion-collapse collapse" aria-labelledby="' + accordionId + '-heading-source" data-bs-parent="#' + accordionId + '"><div class="accordion-body"><div class="row g-3">' +
|
||||
'<div class="col-12 col-md-3"><label class="form-label" for="region_chart_source_sort_' + region.id + '">Order</label><select id="region_chart_source_sort_' + region.id + '" name="region_chart_source_sort_' + region.id + '" class="form-select"><option value="none"' + (sourceOptions.sort === 'none' ? ' selected' : '') + '>Original order</option><option value="reverse-original"' + (sourceOptions.sort === 'reverse-original' ? ' selected' : '') + '>Reverse original order</option><option value="value-ascending"' + (sourceOptions.sort === 'value-ascending' ? ' selected' : '') + '>Increasing values</option><option value="value-descending"' + (sourceOptions.sort === 'value-descending' ? ' selected' : '') + '>Decreasing values</option></select></div>' +
|
||||
'<div class="col-12 col-md-3"><label class="form-label" for="region_chart_source_limit_' + region.id + '">Maximum items</label><input id="region_chart_source_limit_' + region.id + '" name="region_chart_source_limit_' + region.id + '" class="form-control" type="number" min="0" max="500" value="' + escapeHtml(sourceOptions.limit || 0) + '" placeholder="All" /></div>' +
|
||||
'<div class="col-12 col-md-3"><label class="form-label" for="region_chart_source_missing_' + region.id + '">Missing values</label><select id="region_chart_source_missing_' + region.id + '" name="region_chart_source_missing_' + region.id + '" class="form-select"><option value="gap"' + (sourceOptions.missing === 'gap' ? ' selected' : '') + '>Leave gaps</option><option value="skip"' + (sourceOptions.missing === 'skip' ? ' selected' : '') + '>Skip row</option><option value="zero"' + (sourceOptions.missing === 'zero' ? ' selected' : '') + '>Use zero</option></select></div>' +
|
||||
'</div></div></div></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function buildPreviewRenderContext(region, card, existingContent, sources) {
|
||||
var current = getCurrentConfig(region, existingContent || {});
|
||||
function value(name, fallback) {
|
||||
var input = card && card.querySelector ? card.querySelector('[name="' + name + '_' + region.id + '"]') : null;
|
||||
return input ? input.value : fallback;
|
||||
}
|
||||
function checked(name, fallback) {
|
||||
var input = card && card.querySelector ? card.querySelector('[name="' + name + '_' + region.id + '"]') : null;
|
||||
return input ? input.checked : fallback;
|
||||
}
|
||||
return {
|
||||
type: 'chart',
|
||||
chart_type: value('region_chart_type', current.type),
|
||||
source_kind: value('region_chart_source_kind', current.source_kind),
|
||||
source_id: value('region_chart_source_id', current.source_id),
|
||||
items_path: value('region_chart_items_path', current.items_path),
|
||||
label_path: value('region_chart_label_path', current.label_path),
|
||||
value_path: value('region_chart_value_path', current.value_path),
|
||||
dataset_label: value('region_chart_dataset_label', current.dataset_label),
|
||||
color: value('region_chart_color', current.color),
|
||||
datasets: parseRows(value('region_chart_datasets', current.datasets_json)),
|
||||
manual_rows: parseRows(value('region_chart_manual_rows', current.manual_rows_json)),
|
||||
chart_options: {
|
||||
legend: value('region_chart_legend', current.chart_options.legend),
|
||||
begin_at_zero: checked('region_chart_begin_at_zero', current.chart_options.begin_at_zero),
|
||||
y_axis_min: value('region_chart_y_axis_min', current.chart_options.y_axis_min),
|
||||
y_axis_max: value('region_chart_y_axis_max', current.chart_options.y_axis_max),
|
||||
line_points: value('region_chart_line_points', current.chart_options.line_points),
|
||||
line_fill: checked('region_chart_line_fill', current.chart_options.line_fill),
|
||||
bar_orientation: value('region_chart_bar_orientation', current.chart_options.bar_orientation),
|
||||
value_format: value('region_chart_value_format', current.chart_options.value_format),
|
||||
decimal_places: value('region_chart_decimal_places', current.chart_options.decimal_places),
|
||||
text_size: value('region_chart_text_size', current.chart_options.text_size),
|
||||
grid_color: value('region_chart_grid_color', current.chart_options.grid_color),
|
||||
text_color: value('region_chart_text_color', current.chart_options.text_color)
|
||||
},
|
||||
source_options: {
|
||||
sort: value('region_chart_source_sort', current.source_options.sort),
|
||||
limit: value('region_chart_source_limit', current.source_options.limit),
|
||||
reverse: false,
|
||||
missing: value('region_chart_source_missing', current.source_options.missing)
|
||||
},
|
||||
sources: Array.isArray(sources) ? sources : []
|
||||
};
|
||||
}
|
||||
|
||||
registry.register('chart', {
|
||||
label: 'Chart',
|
||||
getDefaultRegionSize: function () { return { width: 480, height: 280 }; },
|
||||
getCurrentConfig: getCurrentConfig,
|
||||
renderPreview: renderPreview,
|
||||
initPreview: initPreview,
|
||||
bindEditorCard: bindEditorCard,
|
||||
renderEditorCard: renderEditorCard,
|
||||
buildPreviewRenderContext: buildPreviewRenderContext
|
||||
});
|
||||
}());
|
||||
@@ -42,14 +42,6 @@ function getColumnsPerRow() {
|
||||
return window.innerWidth >= 1200 ? 6 : window.innerWidth >= 992 ? 3 : window.innerWidth >= 576 ? 2 : 1;
|
||||
}
|
||||
|
||||
function showUploadMessage(message) {
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast(message, 'warning');
|
||||
return;
|
||||
}
|
||||
window.alert(message);
|
||||
}
|
||||
|
||||
function showModal(target) {
|
||||
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
|
||||
window.pulseModal.show(target);
|
||||
@@ -229,12 +221,12 @@ function setUploadFile(file, shouldCrop) {
|
||||
const matchesType = allowedTypes.length === 0 || allowedTypes.indexOf(fileType) !== -1;
|
||||
const maxBytes = fileType.indexOf('video/') === 0 ? Number(uploadInput.dataset.videoMaxBytes) : Number(uploadInput.dataset.imageMaxBytes);
|
||||
if (!matchesType) {
|
||||
showUploadMessage('This media type is disabled in Media Uploads settings.');
|
||||
window.PulseUpload.showMessage('This media type is disabled in Media Uploads settings.');
|
||||
uploadInput.value = '';
|
||||
return;
|
||||
}
|
||||
if (maxBytes && file.size > maxBytes) {
|
||||
showUploadMessage('This file is larger than the configured upload limit.');
|
||||
window.PulseUpload.showMessage('This file is larger than the current upload limit of ' + window.PulseUpload.formatUploadLimit(maxBytes) + '.');
|
||||
uploadInput.value = '';
|
||||
return;
|
||||
}
|
||||
@@ -310,7 +302,7 @@ if (cropApply) {
|
||||
const outputType = cropSourceFile.type === 'image/jpeg' || cropSourceFile.type === 'image/webp' ? cropSourceFile.type : 'image/png';
|
||||
cropper.getCroppedCanvas().toBlob(function (blob) {
|
||||
if (!blob) {
|
||||
showUploadMessage('Unable to crop this image.');
|
||||
window.PulseUpload.showMessage('Unable to crop this image.');
|
||||
return;
|
||||
}
|
||||
const extension = outputType === 'image/jpeg' ? '.jpg' : outputType === 'image/webp' ? '.webp' : '.png';
|
||||
@@ -372,7 +364,7 @@ if (uploadForm) {
|
||||
uploadForm.addEventListener('submit', function (event) {
|
||||
if (uploadInput && !uploadInput.files.length) {
|
||||
event.preventDefault();
|
||||
showUploadMessage('Choose an image or video to upload.');
|
||||
window.PulseUpload.showMessage('Choose an image or video to upload.');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -476,7 +468,7 @@ async function refreshGallery() {
|
||||
visibleRowLimit = 4;
|
||||
updateSearchResults();
|
||||
} catch (error) {
|
||||
showUploadMessage(error.message || 'Unable to refresh media.');
|
||||
window.PulseUpload.showMessage(error.message || 'Unable to refresh media.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,7 +491,7 @@ if (gallery) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (column.dataset.mediaUsed === 'true') {
|
||||
showUploadMessage('This media is in use and cannot be selected.');
|
||||
window.PulseUpload.showMessage('This media is in use and cannot be selected.');
|
||||
return;
|
||||
}
|
||||
const id = String(column.dataset.mediaId || '');
|
||||
@@ -525,7 +517,7 @@ if (loadMoreButton) {
|
||||
visibleRowLimit += 4;
|
||||
updateSearchResults();
|
||||
} catch (error) {
|
||||
showUploadMessage(error.message || 'Unable to load more media.');
|
||||
window.PulseUpload.showMessage(error.message || 'Unable to load more media.');
|
||||
} finally {
|
||||
loadMoreButton.disabled = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
(function (window) {
|
||||
var uploadUtils = window.PulseUpload || {};
|
||||
|
||||
uploadUtils.formatUploadLimit = function (bytes) {
|
||||
var megabytes = Number(bytes || 0) / 1024 / 1024;
|
||||
if (megabytes >= 1024 && megabytes % 1024 === 0) {
|
||||
return (megabytes / 1024) + ' GB';
|
||||
}
|
||||
return (Number.isInteger(megabytes) ? megabytes : megabytes.toFixed(2).replace(/0+$/, '').replace(/\.$/, '')) + ' MB';
|
||||
};
|
||||
|
||||
uploadUtils.showMessage = function (message) {
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast(message, 'warning');
|
||||
return;
|
||||
}
|
||||
window.alert(message);
|
||||
};
|
||||
|
||||
window.PulseUpload = uploadUtils;
|
||||
})(window);
|
||||
@@ -615,7 +615,7 @@ export function createSlideFormEditorController(options) {
|
||||
promotion: false,
|
||||
relative_urls: false,
|
||||
remove_script_host: false,
|
||||
convert_urls: true,
|
||||
convert_urls: false,
|
||||
paste_data_images: false,
|
||||
paste_block_drop: true,
|
||||
resize: false,
|
||||
|
||||
@@ -539,7 +539,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
if (module && typeof module.buildEditorCardContext === 'function') {
|
||||
return module.buildEditorCardContext({
|
||||
region: region,
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'time-date' || region.region_type === 'timetable' || region.region_type === 'weather' ? (existingContent[region.region_key] || {}) : getCurrentRegionValue(region),
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'time-date' || region.region_type === 'timetable' || region.region_type === 'weather' || region.region_type === 'chart' ? (existingContent[region.region_key] || {}) : getCurrentRegionValue(region),
|
||||
currentDuration: getCurrentRegionVideoDuration(region),
|
||||
regionRatio: reduceAspectRatio(region.width, region.height),
|
||||
uploadMaxLabel: uploadMaxLabel,
|
||||
@@ -548,7 +548,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
uploadVideoMaxBytes: uploadVideoMaxBytes,
|
||||
uploadMimeTypes: uploadMimeTypes,
|
||||
disableAudio: (existingContent[region.region_key] || {}).disable_audio,
|
||||
config: region.region_type === 'api' ? getCurrentApiConfig(region) : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : region.region_type === 'weather' ? { weather_location_id: (existingContent[region.region_key] || {}).weather_location_id || '' } : getCurrentRssConfig(region),
|
||||
config: region.region_type === 'api' ? getCurrentApiConfig(region) : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : region.region_type === 'weather' ? { weather_location_id: (existingContent[region.region_key] || {}).weather_location_id || '' } : region.region_type === 'chart' && module && typeof module.getCurrentConfig === 'function' ? module.getCurrentConfig(region, existingContent) : getCurrentRssConfig(region),
|
||||
style: getCurrentTextStyle(region),
|
||||
fontSize: getCurrentTextStyle(region).font_size,
|
||||
feedOptions: rssFeeds.map(function (feed) {
|
||||
@@ -584,7 +584,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
|
||||
return {
|
||||
region: region,
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'weather' ? currentContent : getCurrentRegionValue(region),
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'weather' || region.region_type === 'chart' ? currentContent : getCurrentRegionValue(region),
|
||||
currentDuration: getCurrentRegionVideoDuration(region),
|
||||
regionRatio: reduceAspectRatio(region.width, region.height),
|
||||
uploadMaxLabel: uploadMaxLabel,
|
||||
@@ -593,7 +593,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
uploadVideoMaxBytes: uploadVideoMaxBytes,
|
||||
uploadMimeTypes: uploadMimeTypes,
|
||||
disableAudio: currentContent.disable_audio,
|
||||
config: region.region_type === 'api' ? apiConfig : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : region.region_type === 'weather' ? { weather_location_id: currentContent.weather_location_id || '' } : rssConfig,
|
||||
config: region.region_type === 'api' ? apiConfig : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : region.region_type === 'weather' ? { weather_location_id: currentContent.weather_location_id || '' } : region.region_type === 'chart' ? ((getRegionTypeModule('chart') || {}).getCurrentConfig ? getRegionTypeModule('chart').getCurrentConfig(region, existingContent) : {}) : rssConfig,
|
||||
style: textStyle,
|
||||
fontSize: textStyle.font_size,
|
||||
feedOptions: rssFeeds.map(function (feed) {
|
||||
@@ -636,11 +636,15 @@ export function createSlideFormRegionHelpers(options) {
|
||||
Array.prototype.forEach.call(templateFields.querySelectorAll('[data-region-id]'), function (card) {
|
||||
var module = null;
|
||||
|
||||
if (!card || !card.querySelector || !card.querySelector('input[type="url"][name^="region_qr_code_"]')) {
|
||||
if (!card || !card.querySelector) {
|
||||
return;
|
||||
}
|
||||
|
||||
module = getRegionTypeModule('qr-code');
|
||||
if (card.querySelector('input[type="url"][name^="region_qr_code_"]')) {
|
||||
module = getRegionTypeModule('qr-code');
|
||||
} else if (card.querySelector('select[name^="region_chart_source_kind_"]')) {
|
||||
module = getRegionTypeModule('chart');
|
||||
}
|
||||
|
||||
if (module && typeof module.bindEditorCard === 'function') {
|
||||
module.bindEditorCard({
|
||||
|
||||
@@ -6,14 +6,6 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
||||
(function () {
|
||||
var DEFAULT_FONT_SIZE = 32;
|
||||
|
||||
function formatUploadLimitLabel(bytes) {
|
||||
var megabytes = Number(bytes || 0) / 1024 / 1024;
|
||||
if (megabytes >= 1024 && megabytes % 1024 === 0) {
|
||||
return String(megabytes / 1024) + ' GB';
|
||||
}
|
||||
return (Number.isInteger(megabytes) ? String(megabytes) : megabytes.toFixed(2).replace(/0+$/, '').replace(/\.$/, '')) + ' MB';
|
||||
}
|
||||
|
||||
var dataElement = document.getElementById('slide-editor-data');
|
||||
if (!dataElement) {
|
||||
return;
|
||||
@@ -48,11 +40,11 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
||||
var openPreviewPopupButton = document.getElementById('open-preview-popup');
|
||||
var uploadLimits = slideEditorData.uploadLimits || {};
|
||||
var uploadMaxBytes = Number(uploadLimits.imageMaxBytes || 100 * 1024 * 1024);
|
||||
var uploadMaxLabel = formatUploadLimitLabel(uploadMaxBytes);
|
||||
var uploadMaxLabel = window.PulseUpload.formatUploadLimit(uploadMaxBytes);
|
||||
var uploadVideoMaxBytes = Number(uploadLimits.videoMaxBytes || 1024 * 1024 * 1024);
|
||||
var uploadVideoMaxLabel = formatUploadLimitLabel(uploadVideoMaxBytes);
|
||||
var uploadVideoMaxLabel = window.PulseUpload.formatUploadLimit(uploadVideoMaxBytes);
|
||||
var wysiwygImageUploadMaxBytes = Number(uploadLimits.wysiwygImageMaxBytes || 2 * 1024 * 1024);
|
||||
var wysiwygImageUploadLimitLabel = formatUploadLimitLabel(wysiwygImageUploadMaxBytes);
|
||||
var wysiwygImageUploadLimitLabel = window.PulseUpload.formatUploadLimit(wysiwygImageUploadMaxBytes);
|
||||
var uploadMimeTypes = Array.isArray(uploadLimits.allowedMimeTypes) ? uploadLimits.allowedMimeTypes : [];
|
||||
var videoDurationCache = Object.create(null);
|
||||
var previewRenderFrame = 0;
|
||||
@@ -690,6 +682,13 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
||||
return '<div class="slide-preview-region' + selected + '" style="left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px;">' + content + '</div>';
|
||||
}).join('');
|
||||
|
||||
regions.forEach(function (region) {
|
||||
var typeModule = getRegionTypeModule(region.region_type);
|
||||
if (typeModule && typeof typeModule.initPreview === 'function') {
|
||||
typeModule.initPreview(slidePreviewOverlay);
|
||||
}
|
||||
});
|
||||
|
||||
startPreviewVideoPlayback(slidePreviewOverlay);
|
||||
|
||||
syncPopupPreview();
|
||||
|
||||
@@ -1778,6 +1778,9 @@
|
||||
}
|
||||
if (backgroundInput) {
|
||||
backgroundInput.addEventListener('change', function () {
|
||||
if (backgroundInput.dataset.uploadUrl) {
|
||||
return;
|
||||
}
|
||||
var file = backgroundInput.files && backgroundInput.files[0];
|
||||
if (file) {
|
||||
updateBackgroundPreview(file);
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*!
|
||||
* Chart.js v4.5.1
|
||||
* https://www.chartjs.org
|
||||
* (c) 2025 Chart.js Contributors
|
||||
* Released under the MIT License
|
||||
*/
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Jo},get Decimation(){return ta},get Filler(){return ba},get Legend(){return Ma},get SubTitle(){return Pa},get Title(){return ka},get Tooltip(){return Na}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;a<r;a++)e.call(i,t[a],a);else if(o(t))for(l=Object.keys(t),r=l.length,a=0;a<r;a++)e.call(i,t[l[a]],l[a])}function f(t,e){let i,s,n,o;if(!t||!e||t.length!==e.length)return!1;for(i=0,s=t.length;i<s;++i)if(n=t[i],o=e[i],n.datasetIndex!==o.datasetIndex||n.index!==o.index)return!1;return!0}function g(t){if(n(t))return t.map(g);if(o(t)){const e=Object.create(null),i=Object.keys(t),s=i.length;let n=0;for(;n<s;++n)e[i[n]]=g(t[i[n]]);return e}return t}function p(t){return-1===["__proto__","prototype","constructor"].indexOf(t)}function m(t,e,i,s){if(!p(t))return;const n=e[t],a=i[t];o(n)&&o(a)?x(n,a,s):e[t]=g(a)}function x(t,e,i){const s=n(e)?e:[e],a=s.length;if(!o(t))return t;const r=(i=i||{}).merger||m;let l;for(let e=0;e<a;++e){if(l=s[e],!o(l))continue;const n=Object.keys(l);for(let e=0,s=n.length;e<s;++e)r(n[e],t,l,i)}return t}function b(t,e){return x(t,e,{merger:_})}function _(t,e,i){if(!p(t))return;const s=e[t],n=i[t];o(s)&&o(n)?b(s,n):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=g(n))}const y={"":t=>t,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)<i}function B(t){const e=Math.round(t);t=V(t,e,t/1e3)?e:t;const i=Math.pow(10,Math.floor(z(t))),s=t/i;return(s<=1?1:s<=2?2:s<=5?5:10)*i}function W(t){const e=[],i=Math.sqrt(t);let s;for(s=1;s<i;s++)t%s==0&&(e.push(s),e.push(t/s));return i===(0|i)&&e.push(i),e.sort(((t,e)=>t-e)).pop(),e}function N(t){return!function(t){return"symbol"==typeof t||"object"==typeof t&&null!==t&&!(Symbol.toPrimitive in t||"toString"in t||"valueOf"in t)}(t)&&!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;s<n;s++)o=t[s][i],isNaN(o)||(e.min=Math.min(e.min,o),e.max=Math.max(e.max,o))}function $(t){return t*(C/180)}function Y(t){return t*(180/C)}function U(t){if(!a(t))return;let e=1,i=0;for(;Math.round(t*e)/e!==t;)e*=10,i++;return i}function X(t,e){const i=e.x-t.x,s=e.y-t.y,n=Math.sqrt(i*i+s*s);let o=Math.atan2(s,i);return o<-.5*C&&(o+=O),{angle:o,distance:n}}function q(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function K(t,e){return(t-e+A)%O-C}function G(t){return(t%O+O)%O}function J(t,e,i,s){const n=G(t),o=G(e),a=G(i),r=G(o-n),l=G(a-n),h=G(n-o),c=G(n-a);return n===o||n===a||s&&o===a||r>l&&h<c}function Z(t,e,i){return Math.max(e,Math.min(i,t))}function Q(t){return Z(t,-32768,32767)}function tt(t,e,i,s=1e-6){return t>=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]<e);let s,n=t.length-1,o=0;for(;n-o>1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return n<i||n===i&&t[s+1][e]===i}:s=>t[s][e]<i),st=(t,e,i)=>et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;s<n&&t[s]<e;)s++;for(;n>s&&t[n-1]>i;)n--;return s>0||n<t.length?t.slice(s,n):t}const ot=["push","pop","shift","splice","unshift"];function at(t,e){t._chartjs?t._chartjs.listeners.push(e):(Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),ot.forEach((e=>{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...eLine truncated
|
||||
/*!
|
||||
* @kurkle/color v0.3.2
|
||||
* https://github.com/kurkle/color#readme
|
||||
* (c) 2023 Jukka Kurkela
|
||||
* Released under the MIT License
|
||||
*/function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e<i?6:0):e===n?(i-t)/s+2:(t-e)/s+4}(e,i,s,h,n),r=60*r+.5),[0|r,l||0,a]}function zt(t,e,i,s){return(Array.isArray(e)?t(e[0],e[1],e[2]):t(e,i,s)).map(Mt)}function Ft(t,e,i){return zt(Lt,t,e,i)}function Vt(t){return(t%360+360)%360}function Bt(t){const e=Tt.exec(t);let i,s=255;if(!e)return;e[5]!==i&&(s=e[6]?vt(+e[5]):Mt(+e[5]));const n=Vt(+e[2]),o=+e[3]/100,a=+e[4]/100;return i="hwb"===e[1]?function(t,e,i){return zt(Rt,t,e,i)}(n,o,a):"hsv"===e[1]?function(t,e,i){return zt(Et,t,e,i)}(n,o,a):Ft(n,o,a),{r:i[0],g:i[1],b:i[2],a:s}}const Wt={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Nt={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};let Ht;function jt(t){Ht||(Ht=function(){const t={},e=Object.keys(Nt),i=Object.keys(Wt);let s,n,o,a,r;for(s=0;s<e.length;s++){for(a=r=e[s],n=0;n<i.length;n++)o=i[n],r=r.replace(o,Wt[o]);o=parseInt(Nt[a],16),t[r]=[o>>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),Line truncated
|
||||
//# sourceMappingURL=chart.umd.js.map
|
||||
@@ -1,6 +1,6 @@
|
||||
// Account password page renderer and return URL normalization.
|
||||
|
||||
const { renderView } = require('../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
function normalizeReturnUrl(value) {
|
||||
const url = String(value || '').trim();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Admin client command routes for connected screens.
|
||||
// Client routing should rely on the shared player registration helpers instead of recreating
|
||||
// URL and heartbeat checks in each route, which makes the screen-target selection consistent.
|
||||
|
||||
const { createPageAuthToken } = require('#src/request-auth');
|
||||
const { normalizeBaseUrl } = require('#src/data/player-registry');
|
||||
|
||||
module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
const pool = deps.pool;
|
||||
@@ -31,17 +34,13 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
}
|
||||
|
||||
function normalizeExplicitPlayerBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
return normalizeBaseUrl(value);
|
||||
}
|
||||
|
||||
function normalizeConnectionBaseUrl(connection) {
|
||||
return normalizeExplicitPlayerBaseUrl(connection && connection.playerPublicBaseUrl);
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
async function fetchPlayerRegistrations() {
|
||||
if (typeof common.fetchPlayerRegistrations !== 'function') {
|
||||
return [];
|
||||
@@ -87,6 +86,9 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve a screen's live connections back to the player's internal base URL when the public URL
|
||||
// is known to the registry. This lets the admin UI target the correct local endpoint without
|
||||
// depending on whichever public URL happened to be reported by the connection itself.
|
||||
async function resolvePlayerBaseUrlsForConnections(connections) {
|
||||
const connectionList = Array.isArray(connections) ? connections : [];
|
||||
const seen = new Set();
|
||||
@@ -119,6 +121,9 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
return normalizeExplicitPlayerBaseUrl(matchedPlayer && matchedPlayer.internal_base_url) || normalizedPublicBaseUrl;
|
||||
}
|
||||
|
||||
// Admin commands may target either a direct player connection or a bridged device. Keep the fan-out
|
||||
// logic explicit so a screen can be controlled through the correct transport without mixing both
|
||||
// path choices into the command payload itself.
|
||||
async function forwardPlayerCommandForConnections(screenSlug, connections, commandPayload, connectionId, deviceId) {
|
||||
const connectionList = Array.isArray(connections) ? connections : [];
|
||||
const targetedConnections = connectionId
|
||||
@@ -186,15 +191,6 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
return forwardPlayerCommand(screenSlug, commandPayload, connectionId || deviceId || undefined);
|
||||
}
|
||||
|
||||
function isRecentPlayerRegistration(player, staleSeconds) {
|
||||
const lastSeenAt = player && player.last_seen_at;
|
||||
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
|
||||
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
|
||||
|
||||
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
|
||||
}
|
||||
|
||||
|
||||
app.post('/clients/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) {
|
||||
try {
|
||||
const slug = String(req.params.slug || '').trim();
|
||||
|
||||
@@ -7,7 +7,7 @@ const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
const { convertWeatherSnapshot } = require('#src/data/weather-units');
|
||||
const { buildAuditChanges } = require('#src/data/audit-log');
|
||||
const { PERMISSION_DENIED_MESSAGE } = require('#src/rbac');
|
||||
const { fetchMediaAssets, registerMediaAssets, syncMediaAssetsFromDirectory, publishMediaAssets, removePendingMediaAssets } = require('#src/web/lib/media/library');
|
||||
const { fetchMediaAssets, createImageVariants, registerMediaAssets, syncMediaAssetsFromDirectory, publishMediaAssets, removePendingMediaAssets } = require('#src/web/lib/media/library');
|
||||
|
||||
module.exports = function registerContentRoutes(app, deps) {
|
||||
const pool = deps.pool;
|
||||
@@ -126,6 +126,7 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
function requireSlideUploadPermission(req, res, next) {
|
||||
@@ -147,6 +148,25 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
function requireTemplateUploadPermission(req, res, next) {
|
||||
if (!req.currentUser) {
|
||||
if (typeof setAuthMessageCookie === 'function') {
|
||||
setAuthMessageCookie(res, 'Please sign in to continue.');
|
||||
return res.redirect('/login');
|
||||
}
|
||||
return res.redirect('/login');
|
||||
}
|
||||
|
||||
if (hasAnyPermission(req.currentUser, ['templates.create', 'templates.update'])) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const error = new Error(PERMISSION_DENIED_MESSAGE);
|
||||
error.statusCode = 403;
|
||||
error.expose = true;
|
||||
next(error);
|
||||
}
|
||||
|
||||
function getUploadedFileMediaType(file, uploadContext) {
|
||||
const mimeType = String(file && file.mimetype || '').trim().toLowerCase();
|
||||
const extension = path.extname(String(file && file.originalname || '')).toLowerCase();
|
||||
@@ -233,6 +253,23 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function processImmediateMediaUpload(req, res, uploadContext) {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file was uploaded.' });
|
||||
}
|
||||
|
||||
await validateUploadedFiles([req.file], uploadContext);
|
||||
await createImageVariants(req.file);
|
||||
await registerMediaAssets(pool, [req.file], function (file) {
|
||||
return '/media/uploads/' + file.filename;
|
||||
}, getUploadedFileMediaType, getAuditUserId(req), { published: false });
|
||||
res.json({
|
||||
path: '/media/uploads/' + req.file.filename,
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname || ''
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchTemplateSlideCount(templateId) {
|
||||
const [rows] = await pool.query('SELECT COUNT(*) AS slide_count FROM c_slides WHERE template_id = ?', [templateId]);
|
||||
return Number(rows[0] && rows[0].slide_count) || 0;
|
||||
@@ -268,7 +305,7 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
|
||||
return Array.from(usage);
|
||||
}
|
||||
const { buildDuplicateTemplateName, buildDuplicateTemplate } = require('../signage/templates/duplicate');
|
||||
const { buildDuplicateTemplateName, buildDuplicateTemplate } = require('#src/web/routes/signage/templates/duplicate');
|
||||
|
||||
async function fetchSlidesByTemplateId(templateId) {
|
||||
const [slides] = await pool.query('SELECT id, thumbnail_path FROM c_slides WHERE template_id = ? ORDER BY id ASC', [templateId]);
|
||||
@@ -291,6 +328,7 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
}
|
||||
|
||||
const canvasSizes = await common.fetchCanvasSizesData(pool);
|
||||
const settings = await fetchAppSettings(pool);
|
||||
let duplicateName = buildDuplicateTemplateName(template.name);
|
||||
let duplicateIndex = 2;
|
||||
while (await common.fetchDuplicateName(pool, 'c_templates', duplicateName)) {
|
||||
@@ -298,10 +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));
|
||||
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));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
});
|
||||
async function getTemplateRegionDeleteBlockMessage(template, nextRegions) {
|
||||
const removedRegionKeys = getRemovedTemplateRegionKeys(template && template.regions ? template.regions : [], nextRegions);
|
||||
@@ -445,22 +484,8 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
|
||||
app.post('/slides/uploads', requireSlideUploadPermission, upload.single('file'), async function (req, res, next) {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file was uploaded.' });
|
||||
}
|
||||
|
||||
const uploadContext = String(req.get('X-Upload-Context') || req.query.context || '').trim().toLowerCase();
|
||||
|
||||
await validateUploadedFiles([req.file], uploadContext);
|
||||
await registerMediaAssets(pool, [req.file], function (file) {
|
||||
return '/media/uploads/' + file.filename;
|
||||
}, getUploadedFileMediaType, getAuditUserId(req), { published: false });
|
||||
|
||||
res.json({
|
||||
path: '/media/uploads/' + req.file.filename,
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname || ''
|
||||
});
|
||||
await processImmediateMediaUpload(req, res, uploadContext);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
@@ -485,9 +510,39 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/templates/uploads', requireTemplateUploadPermission, upload.single('file'), async function (req, res, next) {
|
||||
try {
|
||||
await processImmediateMediaUpload(req, res, 'template-background');
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/templates/uploads/cleanup', requireTemplateUploadPermission, async function (req, res, next) {
|
||||
try {
|
||||
const uploadPaths = Array.isArray(req.body && req.body.uploadPaths)
|
||||
? req.body.uploadPaths
|
||||
: req.body && req.body.uploadPaths
|
||||
? [req.body.uploadPaths]
|
||||
: [];
|
||||
|
||||
if (!uploadPaths.length) {
|
||||
return res.status(400).json({ error: 'No upload paths were provided.' });
|
||||
}
|
||||
|
||||
await removePendingMediaAssets(pool, deps.uploadDir, uploadPaths);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/slides', requirePermission('slides.create'), upload.any(), async function (req, res, next) {
|
||||
try {
|
||||
await validateUploadedFiles(req.files || []);
|
||||
const settings = await validateUploadedFiles(req.files || []);
|
||||
for (const file of req.files || []) {
|
||||
await createImageVariants(file);
|
||||
}
|
||||
await registerMediaAssets(pool, req.files, function (file) {
|
||||
return '/media/uploads/' + file.filename;
|
||||
}, getUploadedFileMediaType, getAuditUserId(req));
|
||||
@@ -523,7 +578,10 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
|
||||
app.post('/slides/:id', requirePermission('slides.update'), upload.any(), async function (req, res, next) {
|
||||
try {
|
||||
await validateUploadedFiles(req.files || []);
|
||||
const settings = await validateUploadedFiles(req.files || []);
|
||||
for (const file of req.files || []) {
|
||||
await createImageVariants(file);
|
||||
}
|
||||
await registerMediaAssets(pool, req.files, function (file) {
|
||||
return '/media/uploads/' + file.filename;
|
||||
}, getUploadedFileMediaType, getAuditUserId(req));
|
||||
@@ -632,7 +690,8 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
const data = await common.fetchCanvasSizesData(pool);
|
||||
await syncMediaAssetsFromDirectory(pool, deps.uploadDir);
|
||||
const mediaAssets = (await fetchMediaAssets(pool)).slice(0, 24);
|
||||
res.send(pages.renderTemplateAddPage(null, req.query.message ? String(req.query.message) : '', data.canvasSizes, req.currentUser, mediaAssets));
|
||||
const settings = await fetchAppSettings(pool);
|
||||
res.send(pages.renderTemplateAddPage(null, req.query.message ? String(req.query.message) : '', data.canvasSizes, req.currentUser, mediaAssets, Number(settings['uploads.image_max_bytes']) || IMAGE_UPLOAD_MAX_BYTES));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
@@ -663,6 +722,7 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
[result.insertId, region.region_key, region.region_type, region.label, region.lock_ratio, JSON.stringify(region.animation_json || {}), region.x, region.y, region.width, region.height, region.z_index, actorId, actorId]
|
||||
);
|
||||
}
|
||||
await publishMediaAssets(pool, collectUploadReferencesFromPayload(payload));
|
||||
await syncPlaylistUploadsOnChange({
|
||||
key: 'template:create:' + result.insertId,
|
||||
pool: pool,
|
||||
@@ -691,7 +751,8 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
const sizeData = await common.fetchCanvasSizesData(pool);
|
||||
await syncMediaAssetsFromDirectory(pool, deps.uploadDir);
|
||||
const mediaAssets = await fetchMediaAssets(pool);
|
||||
res.send(pages.renderTemplateEditPage(template, sizeData, req.query.message ? String(req.query.message) : '', req.currentUser, mediaAssets));
|
||||
const settings = await fetchAppSettings(pool);
|
||||
res.send(pages.renderTemplateEditPage(template, sizeData, req.query.message ? String(req.query.message) : '', req.currentUser, mediaAssets, Number(settings['uploads.image_max_bytes']) || IMAGE_UPLOAD_MAX_BYTES));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
@@ -745,6 +806,7 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
[template.id, region.region_key, region.region_type, region.label, region.lock_ratio, JSON.stringify(region.animation_json || {}), region.x, region.y, region.width, region.height, region.z_index, actorId, actorId]
|
||||
);
|
||||
}
|
||||
await publishMediaAssets(pool, Array.from(new Set(Array.from(existingUploadRefs).concat(Array.from(nextUploadRefs)))));
|
||||
await syncPlaylistUploadsOnChange({
|
||||
key: 'template:update:' + template.id,
|
||||
pool: pool,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Admin manage routes for screens and commands.
|
||||
// Screen command dispatch should not duplicate the global player-registration rules; this route
|
||||
// intentionally relies on the shared registry helpers for consistent URL and freshness checks.
|
||||
|
||||
const { buildAuditChanges } = require('#src/data/audit-log');
|
||||
const { normalizeBaseUrl } = require('#src/data/player-registry');
|
||||
|
||||
module.exports = function registerManageRoutes(app, deps) {
|
||||
const pool = deps.pool;
|
||||
@@ -31,7 +34,7 @@ module.exports = function registerManageRoutes(app, deps) {
|
||||
}
|
||||
|
||||
function normalizeExplicitPlayerBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
return normalizeBaseUrl(value);
|
||||
}
|
||||
|
||||
async function fetchPlayerRegistrations() {
|
||||
@@ -47,14 +50,6 @@ module.exports = function registerManageRoutes(app, deps) {
|
||||
}
|
||||
}
|
||||
|
||||
function isRecentPlayerRegistration(player, staleSeconds) {
|
||||
const lastSeenAt = player && player.last_seen_at;
|
||||
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
|
||||
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
|
||||
|
||||
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
|
||||
}
|
||||
|
||||
async function fetchLiveConnectionsForScreen(slug) {
|
||||
if (typeof getScreenConnections !== 'function') {
|
||||
return [];
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Admin page route composition for the signage sections.
|
||||
|
||||
module.exports = function registerPagesRoutes(app, deps) {
|
||||
require('../signage/dashboard/routes')(app, deps);
|
||||
require('../signage/clients/routes')(app, deps);
|
||||
require('../signage/screens/routes')(app, deps);
|
||||
require('../signage/playlists/routes')(app, deps);
|
||||
require('../signage/slides/routes')(app, deps);
|
||||
require('../signage/canvas-sizes/routes')(app, deps);
|
||||
require('../signage/templates/routes')(app, deps);
|
||||
require('#src/web/routes/signage/dashboard/routes')(app, deps);
|
||||
require('#src/web/routes/signage/clients/routes')(app, deps);
|
||||
require('#src/web/routes/signage/screens/routes')(app, deps);
|
||||
require('#src/web/routes/signage/playlists/routes')(app, deps);
|
||||
require('#src/web/routes/signage/slides/routes')(app, deps);
|
||||
require('#src/web/routes/signage/canvas-sizes/routes')(app, deps);
|
||||
require('#src/web/routes/signage/templates/routes')(app, deps);
|
||||
};
|
||||
@@ -12,9 +12,9 @@
|
||||
const permissions = Array.isArray(deps.permissions) ? deps.permissions : [];
|
||||
const readArrayField = deps.readArrayField;
|
||||
const normalizePermissionKeys = deps.normalizePermissionKeys;
|
||||
const { buildPagination } = require('../../lib/pagination');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const requirePermission = deps.requirePermission;
|
||||
const { buildDuplicateRoleName, buildDuplicateRole } = require('../settings/rbac/duplicate');
|
||||
const { buildDuplicateRoleName, buildDuplicateRole } = require('#src/web/routes/settings/rbac/duplicate');
|
||||
|
||||
const ROLE_NAME_MAX_LENGTH = 255;
|
||||
const ROLE_DESCRIPTION_MAX_LENGTH = 255;
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
const validatePasswordStrength = deps.validatePasswordStrength;
|
||||
const readArrayField = deps.readArrayField;
|
||||
const rbacData = deps.rbacData;
|
||||
const { buildPagination } = require('../../lib/pagination');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const requirePermission = deps.requirePermission;
|
||||
const { buildDuplicateUserName, buildDuplicateUser } = require('../settings/users/duplicate');
|
||||
const { buildDuplicateUserName, buildDuplicateUser } = require('#src/web/routes/settings/users/duplicate');
|
||||
|
||||
const USER_NAME_MAX_LENGTH = 255;
|
||||
const USER_USERNAME_MAX_LENGTH = 255;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { renderView } = require('../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderAcceptInvitePage(message, token, email, name, passwordRequirements, isValidInvitation) {
|
||||
const requirements = passwordRequirements || { minimumLength: 10, minimumCategories: 3, requireLowercase: false, requireUppercase: false, requireNumber: false, requireSymbol: false };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { renderView } = require('../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderEmailVerificationErrorPage(message) {
|
||||
return renderView('auth/email-verification-error', { title: 'Verification link unavailable', authShell: true, bodyClass: 'login-page-body', message: message || 'This email verification link is invalid or has expired.' });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { renderView } = require('../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderEmailVerifiedPage() {
|
||||
return renderView('auth/email-verified', { title: 'Email verified', authShell: true, bodyClass: 'login-page-body' });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { renderView } = require('../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderForgotPasswordPage(message) {
|
||||
return renderView('auth/forgot-password', { title: 'Reset password', authShell: true, bodyClass: 'login-page-body', message: message || '' });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Authentication route registration for the web app.
|
||||
|
||||
const { normalizeReturnToPath, getRequestOrigin } = require('../../lib/auth/session');
|
||||
const { normalizeReturnToPath, getRequestOrigin } = require('#src/web/lib/auth/session');
|
||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
const { formatAccountEmailExpiry, renderAccountEmailTemplate } = require('#src/data/account-email-templates');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Sign-in page renderer for the authentication flow.
|
||||
|
||||
const { renderView } = require('../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderLoginPage(message, returnTo, username, showForgotPassword) {
|
||||
return renderView('auth/login', {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { renderView } = require('../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderResetPasswordPage(message, token, passwordRequirements) {
|
||||
const requirements = passwordRequirements || { minimumLength: 10, minimumCategories: 3, requireLowercase: false, requireUppercase: false, requireNumber: false, requireSymbol: false };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// API source add page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildApiSourceFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildApiSourceFormViewModel } = require('#src/web/routes/data-sources/api-sources/form-view-model');
|
||||
|
||||
module.exports = function renderApiSourceAddPage(apiSource, message, currentUser, options) {
|
||||
return renderView('data-sources/api-sources/form', buildApiSourceFormViewModel(apiSource, message, currentUser, options, false));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// API source edit page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildApiSourceFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildApiSourceFormViewModel } = require('#src/web/routes/data-sources/api-sources/form-view-model');
|
||||
|
||||
module.exports = function renderApiSourceEditPage(apiSource, data, message, currentUser) {
|
||||
const viewData = Object.assign({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// API source list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
function toIsoTimestamp(value) {
|
||||
if (!value) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// API source route registration.
|
||||
|
||||
const { buildPagination } = require('../../../lib/pagination');
|
||||
const renderApiSourcesPage = require('./list');
|
||||
const renderApiSourceAddPage = require('./add');
|
||||
const renderApiSourceEditPage = require('./edit');
|
||||
const { buildDuplicateApiSourceName, buildDuplicateApiSource } = require('./duplicate');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const renderApiSourcesPage = require('#src/web/routes/data-sources/api-sources/list');
|
||||
const renderApiSourceAddPage = require('#src/web/routes/data-sources/api-sources/add');
|
||||
const renderApiSourceEditPage = require('#src/web/routes/data-sources/api-sources/edit');
|
||||
const { buildDuplicateApiSourceName, buildDuplicateApiSource } = require('#src/web/routes/data-sources/api-sources/duplicate');
|
||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
|
||||
function toIsoTimestamp(value) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// RSS feed add page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildRssFeedFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildRssFeedFormViewModel } = require('#src/web/routes/data-sources/rss-feeds/form-view-model');
|
||||
|
||||
module.exports = function renderRssFeedAddPage(rssFeed, message, currentUser, options) {
|
||||
return renderView('data-sources/rss-feeds/form', buildRssFeedFormViewModel(rssFeed, message, currentUser, options, false));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// RSS feed edit page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildRssFeedFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildRssFeedFormViewModel } = require('#src/web/routes/data-sources/rss-feeds/form-view-model');
|
||||
|
||||
module.exports = function renderRssFeedEditPage(rssFeed, data, message, currentUser) {
|
||||
const viewData = Object.assign({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// RSS feed list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
function formatIntervalLabel(interval, unit) {
|
||||
const value = Math.max(1, Number(interval) || 0);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// RSS feed route registration.
|
||||
|
||||
const { buildPagination } = require('../../../lib/pagination');
|
||||
const renderRssFeedsPage = require('./list');
|
||||
const renderRssFeedAddPage = require('./add');
|
||||
const renderRssFeedEditPage = require('./edit');
|
||||
const { buildDuplicateRssFeedName, buildDuplicateRssFeed } = require('./duplicate');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const renderRssFeedsPage = require('#src/web/routes/data-sources/rss-feeds/list');
|
||||
const renderRssFeedAddPage = require('#src/web/routes/data-sources/rss-feeds/add');
|
||||
const renderRssFeedEditPage = require('#src/web/routes/data-sources/rss-feeds/edit');
|
||||
const { buildDuplicateRssFeedName, buildDuplicateRssFeed } = require('#src/web/routes/data-sources/rss-feeds/duplicate');
|
||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
|
||||
async function getDataSourceUsageMaps(pool, common) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Timetable group add page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildTimetableGroupFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildTimetableGroupFormViewModel } = require('#src/web/routes/data-sources/timetables/form-view-model');
|
||||
|
||||
module.exports = function renderTimetableGroupAddPage(timetableGroup, timetableEntries, message, currentUser, options) {
|
||||
return renderView('data-sources/timetables/form', buildTimetableGroupFormViewModel(timetableGroup, timetableEntries, message, currentUser, false, options));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Timetable group edit page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildTimetableGroupFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildTimetableGroupFormViewModel } = require('#src/web/routes/data-sources/timetables/form-view-model');
|
||||
|
||||
module.exports = function renderTimetableGroupEditPage(timetableGroup, timetableEntries, message, currentUser) {
|
||||
return renderView('data-sources/timetables/form', buildTimetableGroupFormViewModel(timetableGroup, timetableEntries, message, currentUser, true));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Timetable group list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { normalizeTimeZone } = require('#src/data/timetables');
|
||||
|
||||
function formatDateInTimeZone(value, timeZone) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Timetable route registration.
|
||||
|
||||
const { buildPagination } = require('../../../lib/pagination');
|
||||
const renderTimetableGroupsPage = require('./list');
|
||||
const renderTimetableGroupAddPage = require('./add');
|
||||
const renderTimetableGroupEditPage = require('./edit');
|
||||
const { buildDuplicateTimetableGroupName, buildDuplicateTimetableGroup, buildDuplicateTimetableEntry } = require('./duplicate');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const renderTimetableGroupsPage = require('#src/web/routes/data-sources/timetables/list');
|
||||
const renderTimetableGroupAddPage = require('#src/web/routes/data-sources/timetables/add');
|
||||
const renderTimetableGroupEditPage = require('#src/web/routes/data-sources/timetables/edit');
|
||||
const { buildDuplicateTimetableGroupName, buildDuplicateTimetableGroup, buildDuplicateTimetableEntry } = require('#src/web/routes/data-sources/timetables/duplicate');
|
||||
|
||||
async function getDataSourceUsageMaps(pool, common) {
|
||||
const [slides] = await pool.query('SELECT content_json FROM c_slides WHERE content_json IS NOT NULL');
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Weather location source CRUD routes.
|
||||
|
||||
const { buildPagination } = require('../../lib/pagination');
|
||||
const renderWeatherLocationsPage = require('./weather/list');
|
||||
const renderWeatherLocationAddPage = require('./weather/add');
|
||||
const renderWeatherLocationEditPage = require('./weather/edit');
|
||||
const { buildDuplicateWeatherLocationName, buildDuplicateWeatherLocation } = require('./weather/duplicate');
|
||||
const { fetchAppSettings } = require('../../../data/app-settings');
|
||||
const { buildAuditChanges } = require('../../../data/audit-log');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const renderWeatherLocationsPage = require('#src/web/routes/data-sources/weather/list');
|
||||
const renderWeatherLocationAddPage = require('#src/web/routes/data-sources/weather/add');
|
||||
const renderWeatherLocationEditPage = require('#src/web/routes/data-sources/weather/edit');
|
||||
const { buildDuplicateWeatherLocationName, buildDuplicateWeatherLocation } = require('#src/web/routes/data-sources/weather/duplicate');
|
||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
const { buildAuditChanges } = require('#src/data/audit-log');
|
||||
|
||||
async function getWeatherLocationUsageIds(pool, common) {
|
||||
const [slides] = await pool.query('SELECT content_json FROM c_slides WHERE content_json IS NOT NULL');
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
const { renderView } = require('../../../view');
|
||||
const buildWeatherLocationFormViewModel = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const buildWeatherLocationFormViewModel = require('#src/web/routes/data-sources/weather/form-view-model');
|
||||
module.exports = function renderWeatherLocationAddPage(location, message, currentUser, options) { return renderView('data-sources/weather/form', buildWeatherLocationFormViewModel(location, message, currentUser, false, options)); };
|
||||
@@ -1,3 +1,3 @@
|
||||
const { renderView } = require('../../../view');
|
||||
const buildWeatherLocationFormViewModel = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const buildWeatherLocationFormViewModel = require('#src/web/routes/data-sources/weather/form-view-model');
|
||||
module.exports = function renderWeatherLocationEditPage(location, message, currentUser, options) { return renderView('data-sources/weather/form', buildWeatherLocationFormViewModel(location, message, currentUser, true, options)); };
|
||||
@@ -1,4 +1,4 @@
|
||||
const { convertWeatherSnapshot } = require('../../../../data/weather-units');
|
||||
const { convertWeatherSnapshot } = require('#src/data/weather-units');
|
||||
|
||||
function buildDefaultWeatherLocation() {
|
||||
return { id: null, enabled: 1, name: '', location_label: '', latitude: '', longitude: '', timezone: 'Europe/London', provider: 'open-meteo', temperature_unit: 'celsius', wind_unit: 'kmh', precipitation_unit: 'mm', update_interval_value: 30, update_interval_unit: 'minutes' };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
function formatInterval(value, unit) {
|
||||
const amount = Math.max(1, Number(value) || 0);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const { verifyRequestAuth } = require('#src/request-auth');
|
||||
const { collectFontLibrarySyncOperations } = require('#src/web/lib/media/font-library');
|
||||
const { collectLocalControlUsers } = require('../../lib/local-control-users');
|
||||
const { collectLocalControlUsers } = require('#src/web/lib/local-control-users');
|
||||
|
||||
function requireRequestAuth(req, res, next) {
|
||||
if (!verifyRequestAuth(req)) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const { fetchPlayerRegistrations } = require('#src/data/player-registry');
|
||||
const { renderView } = require('../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
function wait(milliseconds) {
|
||||
return new Promise(function (resolve) {
|
||||
|
||||
+24
-24
@@ -1,28 +1,28 @@
|
||||
// Compose the primary web route registrations.
|
||||
|
||||
const registerAuthRoutes = require('./auth');
|
||||
const registerPagesRoutes = require('./admin/pages');
|
||||
const registerAccountRoutes = require('./admin/account');
|
||||
const registerUsersRoutes = require('./admin/users');
|
||||
const registerPlaylistRoutes = require('./admin/playlists');
|
||||
const registerManageRoutes = require('./admin/manage');
|
||||
const registerScreenCommandRoutes = require('./admin/client-commands');
|
||||
const registerAnnouncementRoutes = require('./signage/announcements/routes');
|
||||
const registerContentRoutes = require('./admin/content');
|
||||
const registerRootDataSourceRoutes = require('./admin/data-sources');
|
||||
const registerApiSourceRoutes = require('./data-sources/api-sources/routes');
|
||||
const registerRssFeedRoutes = require('./data-sources/rss-feeds/routes');
|
||||
const registerTimetableRoutes = require('./data-sources/timetables/routes');
|
||||
const registerWeatherRoutes = require('./data-sources/weather');
|
||||
const registerSettingsRoutes = require('./settings/background-tasks');
|
||||
const registerFontRoutes = require('./settings/fonts');
|
||||
const registerMediaLibraryRoutes = require('./settings/media-library');
|
||||
const registerSettingsPageRoutes = require('./settings/routes');
|
||||
const registerAuditLogRoutes = require('./settings/audit-log');
|
||||
const registerAboutRoutes = require('./settings/about/routes');
|
||||
const registerInternalSyncRoutes = require('./internal/sync');
|
||||
const registerScreensRoutes = require('./signage/screens/routes');
|
||||
const registerOnboardingRoutes = require('./onboarding');
|
||||
const registerAuthRoutes = require('#src/web/routes/auth/index');
|
||||
const registerPagesRoutes = require('#src/web/routes/admin/pages');
|
||||
const registerAccountRoutes = require('#src/web/routes/admin/account');
|
||||
const registerUsersRoutes = require('#src/web/routes/admin/users');
|
||||
const registerPlaylistRoutes = require('#src/web/routes/admin/playlists');
|
||||
const registerManageRoutes = require('#src/web/routes/admin/manage');
|
||||
const registerScreenCommandRoutes = require('#src/web/routes/admin/client-commands');
|
||||
const registerAnnouncementRoutes = require('#src/web/routes/signage/announcements/routes');
|
||||
const registerContentRoutes = require('#src/web/routes/admin/content');
|
||||
const registerRootDataSourceRoutes = require('#src/web/routes/admin/data-sources');
|
||||
const registerApiSourceRoutes = require('#src/web/routes/data-sources/api-sources/routes');
|
||||
const registerRssFeedRoutes = require('#src/web/routes/data-sources/rss-feeds/routes');
|
||||
const registerTimetableRoutes = require('#src/web/routes/data-sources/timetables/routes');
|
||||
const registerWeatherRoutes = require('#src/web/routes/data-sources/weather');
|
||||
const registerSettingsRoutes = require('#src/web/routes/settings/background-tasks');
|
||||
const registerFontRoutes = require('#src/web/routes/settings/fonts');
|
||||
const registerMediaLibraryRoutes = require('#src/web/routes/settings/media-library');
|
||||
const registerSettingsPageRoutes = require('#src/web/routes/settings/routes');
|
||||
const registerAuditLogRoutes = require('#src/web/routes/settings/audit-log');
|
||||
const registerAboutRoutes = require('#src/web/routes/settings/about/routes');
|
||||
const registerInternalSyncRoutes = require('#src/web/routes/internal/sync');
|
||||
const registerScreensRoutes = require('#src/web/routes/signage/screens/routes');
|
||||
const registerOnboardingRoutes = require('#src/web/routes/onboarding');
|
||||
const { PERMISSIONS, normalizePermissionKeys } = require('#src/rbac');
|
||||
|
||||
function registerRoutes(app, deps) {
|
||||
@@ -209,7 +209,7 @@ function registerSignageRoutes(app, deps) {
|
||||
|
||||
function registerSettingsAndContentRoutes(app, deps) {
|
||||
// Content, data-source, RBAC, and settings routes share the admin-side services.
|
||||
const registerRbacRoutes = require('./admin/rbac');
|
||||
const registerRbacRoutes = require('#src/web/routes/admin/rbac');
|
||||
|
||||
registerRbacRoutes(app, {
|
||||
pool: deps.pool,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const packageLock = require('#root/package-lock.json');
|
||||
const tinyMcePackage = require('#root/src/web/public/vendor/tinymce/package.json');
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
const fs = require('fs');
|
||||
const { hasAnyPermission, PERMISSION_DENIED_MESSAGE } = require('#src/rbac');
|
||||
const fontLibrary = require('#src/web/lib/media/font-library');
|
||||
const { buildPagination } = require('../../lib/pagination');
|
||||
const { createSearchMatcher, getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber, sortRows } = require('../../lib/list-query');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const { createSearchMatcher, getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber, sortRows } = require('#src/web/lib/list-query');
|
||||
|
||||
const LIST_PAGE_SIZE = 25;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Managed fonts settings page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderFontsPage(data, message, currentUser) {
|
||||
return renderView('settings/fonts/list', {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Visual-only application settings page.
|
||||
|
||||
const { renderView } = require('../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderSettingsPage(settings, suggestedAnnouncementIcons, currentUser, message, mediaSettings, diagnostics) {
|
||||
const iconOptions = Array.isArray(suggestedAnnouncementIcons) ? suggestedAnnouncementIcons : [];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Pending invitations page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderInvitationsPage(data, currentUser) {
|
||||
return renderView('settings/invitations/list', {
|
||||
|
||||
@@ -195,6 +195,7 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
|
||||
: Math.round(maxBytes / (1024 * 1024)) + ' MB';
|
||||
return res.redirect('/media-library?message=' + encodeURIComponent('File must be ' + limitLabel + ' or smaller.'));
|
||||
}
|
||||
await mediaLibrary.createImageVariants(req.file);
|
||||
await mediaLibrary.registerMediaAsset(deps.pool, Object.assign({}, req.file, { mediaType: mediaType }), '/media/uploads/' + req.file.filename, deps.getAuditUserId(req));
|
||||
res.redirect('/media-library?message=' + encodeURIComponent('Media uploaded.'));
|
||||
} catch (error) {
|
||||
@@ -222,6 +223,7 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
await mediaLibrary.removeImageVariants(filePath);
|
||||
if (typeof deps.syncPlaylistUploadsOnChange === 'function') {
|
||||
await deps.syncPlaylistUploadsOnChange({
|
||||
key: 'media-library:delete:' + asset.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Media library page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderMediaLibraryPage(data, message, currentUser) {
|
||||
return renderView('settings/media-library/list', {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// RBAC role add page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildRbacAddViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildRbacAddViewModel } = require('#src/web/routes/settings/rbac/form-view-model');
|
||||
|
||||
module.exports = function renderRbacAddPage(message, currentUser, formValues, permissionGroups, users, pagination, messageVariant) {
|
||||
return renderView('settings/rbac/form', buildRbacAddViewModel(message, currentUser, formValues, permissionGroups, users, pagination, messageVariant));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// RBAC role edit page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildRbacEditViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildRbacEditViewModel } = require('#src/web/routes/settings/rbac/form-view-model');
|
||||
|
||||
module.exports = function renderRbacEditPage(role, message, currentUser, permissionGroups, users, pagination) {
|
||||
return renderView('settings/rbac/form', buildRbacEditViewModel(role, message, currentUser, permissionGroups, users, pagination));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// RBAC role-and-permission list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderRbacPage(data, message, currentUser) {
|
||||
return renderView('settings/rbac/list', {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// User add page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildUsersAddViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildUsersAddViewModel } = require('#src/web/routes/settings/users/form-view-model');
|
||||
|
||||
module.exports = function renderUsersAddPage(message, currentUser, roles, formValues, messageVariant) {
|
||||
return renderView('settings/users/form', buildUsersAddViewModel(message, currentUser, roles, formValues, messageVariant));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// User edit page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildUsersEditViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildUsersEditViewModel } = require('#src/web/routes/settings/users/form-view-model');
|
||||
|
||||
module.exports = function renderUsersEditPage(user, message, currentUser, roles, sessions) {
|
||||
return renderView('settings/users/form', buildUsersEditViewModel(user, message, currentUser, roles, sessions));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderUsersInvitePage(message, currentUser, roles, formValues, messageVariant) {
|
||||
return renderView('settings/users/invite', {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// User list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderUsersPage(data, message, currentUser) {
|
||||
return renderView('settings/users/list', {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Announcement add page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildAnnouncementFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildAnnouncementFormViewModel } = require('#src/web/routes/signage/announcements/form-view-model');
|
||||
|
||||
module.exports = function renderAnnouncementAddPage(announcement, message, currentUser, options) {
|
||||
return renderView('signage/announcements/form', buildAnnouncementFormViewModel(announcement, message, currentUser, options, false));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Announcement edit page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildAnnouncementFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildAnnouncementFormViewModel } = require('#src/web/routes/signage/announcements/form-view-model');
|
||||
|
||||
module.exports = function renderAnnouncementEditPage(announcement, _data, message, currentUser, options) {
|
||||
return renderView('signage/announcements/form', buildAnnouncementFormViewModel(announcement, message, currentUser, options, true));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Announcement list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { ANNOUNCEMENT_ICON_LABELS, DEFAULT_ANNOUNCEMENT_ICON } = require('#src/data/announcement-icons');
|
||||
|
||||
function formatDurationLabel(seconds) {
|
||||
|
||||
@@ -8,11 +8,11 @@ module.exports = function registerAnnouncementRoutes(app, deps) {
|
||||
const getAuditUserId = deps.getAuditUserId;
|
||||
const redirectAfterSave = deps.redirectAfterSave;
|
||||
const requirePermission = deps.requirePermission;
|
||||
const { buildPagination } = require('../../../lib/pagination');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const announcementData = require('#src/data/announcements');
|
||||
const announcementIcons = require('#src/data/announcement-icons');
|
||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
const { buildDuplicateAnnouncementName, buildDuplicateAnnouncement } = require('./duplicate');
|
||||
const { buildDuplicateAnnouncementName, buildDuplicateAnnouncement } = require('#src/web/routes/signage/announcements/duplicate');
|
||||
|
||||
const LIST_PAGE_SIZE = 25;
|
||||
const ANNOUNCEMENT_TYPES = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Canvas size add page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildCanvasSizeFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildCanvasSizeFormViewModel } = require('#src/web/routes/signage/canvas-sizes/form-view-model');
|
||||
|
||||
module.exports = function renderCanvasSizeAddPage(canvasSize, message, currentUser) {
|
||||
return renderView('canvas-sizes/form', buildCanvasSizeFormViewModel(canvasSize, message, currentUser, false));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Canvas size edit page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildCanvasSizeFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildCanvasSizeFormViewModel } = require('#src/web/routes/signage/canvas-sizes/form-view-model');
|
||||
|
||||
module.exports = function renderCanvasSizeEditPage(canvasSize, _data, message, currentUser) {
|
||||
return renderView('canvas-sizes/form', buildCanvasSizeFormViewModel(canvasSize, message, currentUser, true));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Shared canvas size form view-model builder.
|
||||
|
||||
const { MAX_CANVAS_SIZE_DIMENSION } = require('#src/data');
|
||||
const { MAX_CANVAS_SIZE_DIMENSION } = require('#src/data/index');
|
||||
|
||||
function buildDefaultCanvasSize() {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Canvas size list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderCanvasSizesPage(data, message, currentUser) {
|
||||
const canvasSizes = (data.canvasSizes || []).map((size) => ({
|
||||
|
||||
@@ -4,7 +4,7 @@ module.exports = function registerCanvasSizeRoutes(app, deps) {
|
||||
const pages = deps.pages;
|
||||
const common = deps.common;
|
||||
const pool = deps.pool;
|
||||
const { buildPagination } = require('../../../lib/pagination');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
const LIST_PAGE_SIZE = 25;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Connected clients list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderConnectedClientsPage(data, message, currentUser) {
|
||||
return renderView('clients/list', {
|
||||
|
||||
@@ -5,8 +5,8 @@ module.exports = function registerClientsRoutes(app, deps) {
|
||||
const common = deps.common;
|
||||
const pages = deps.pages;
|
||||
const buildDashboardState = deps.buildDashboardState;
|
||||
const { buildPagination } = require('../../../lib/pagination');
|
||||
const { compareSortValues, createSearchMatcher, getComparableSortValue } = require('../../../lib/list-query');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const { compareSortValues, createSearchMatcher, getComparableSortValue } = require('#src/web/lib/list-query');
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
const LIST_PAGE_SIZE = 25;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Dashboard page renderer for the signage overview.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderDashboardPage(data, message, currentUser) {
|
||||
return renderView('dashboard/index', {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Playlist add page renderer.
|
||||
|
||||
const renderPlaylistEditorPage = require('./form-page');
|
||||
const renderPlaylistEditorPage = require('#src/web/routes/signage/playlists/form-page');
|
||||
|
||||
module.exports = async function renderPlaylistFormPage(playlistOrMessage, messageOrCurrentUser, currentUserOrData, dataOrPool, poolMaybe) {
|
||||
const hasPlaylistArgument = arguments.length >= 5;
|
||||
@@ -9,7 +9,7 @@ module.exports = async function renderPlaylistFormPage(playlistOrMessage, messag
|
||||
const currentUser = hasPlaylistArgument ? currentUserOrData : messageOrCurrentUser;
|
||||
const data = hasPlaylistArgument ? dataOrPool : currentUserOrData;
|
||||
const pool = hasPlaylistArgument ? poolMaybe : dataOrPool;
|
||||
const adminData = data || (pool ? await require('#src/data').fetchAdminData(pool) : { slides: [] });
|
||||
const adminData = data || (pool ? await require('#src/data/index').fetchAdminData(pool) : { slides: [] });
|
||||
|
||||
return renderPlaylistEditorPage(playlist, adminData, message, currentUser, { isEdit: false });
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Playlist edit page renderer.
|
||||
|
||||
const renderPlaylistEditorPage = require('./form-page');
|
||||
const renderPlaylistEditorPage = require('#src/web/routes/signage/playlists/form-page');
|
||||
|
||||
module.exports = function renderPlaylistEditPage(playlist, data, message, currentUser) {
|
||||
return renderPlaylistEditorPage(playlist, data || {}, message, currentUser, { isEdit: true });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Shared playlist form page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildPlaylistFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildPlaylistFormViewModel } = require('#src/web/routes/signage/playlists/form-view-model');
|
||||
|
||||
module.exports = function renderPlaylistFormPage(playlist, data, message, currentUser, options) {
|
||||
return renderView('playlists/form', buildPlaylistFormViewModel(playlist || null, data || {}, message, currentUser, options || {}));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Playlist list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderPlaylistsPage(data, message, currentUser) {
|
||||
const playlists = (data.playlists || []).map((playlist) => {
|
||||
|
||||
@@ -5,9 +5,9 @@ module.exports = function registerPlaylistsRoutes(app, deps) {
|
||||
const common = deps.common;
|
||||
const pages = deps.pages;
|
||||
const fetchScreensByPlaylistId = deps.fetchScreensByPlaylistId;
|
||||
const { buildPagination } = require('../../../lib/pagination');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const requirePermission = deps.requirePermission;
|
||||
const { buildDuplicatePlaylistName, buildDuplicatePlaylist } = require('./duplicate');
|
||||
const { buildDuplicatePlaylistName, buildDuplicatePlaylist } = require('#src/web/routes/signage/playlists/duplicate');
|
||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
|
||||
const LIST_PAGE_SIZE = 25;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Shared playlist slide configuration rendering helpers.
|
||||
|
||||
const { renderFragment } = require('../../../view');
|
||||
const { renderFragment } = require('#src/web/view');
|
||||
const { formatScheduleRuleSummary, formatScheduleRulesSummary, normalizeDayValues } = require('#src/web/lib/playlist-schedule');
|
||||
|
||||
const DAY_OPTIONS = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Screen add page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildScreenFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildScreenFormViewModel } = require('#src/web/routes/signage/screens/form-view-model');
|
||||
|
||||
module.exports = function renderScreenFormPage(data, message, currentUser) {
|
||||
return renderView('screens/form', buildScreenFormViewModel(null, data || {}, message, currentUser, false));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Screen edit page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildScreenFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildScreenFormViewModel } = require('#src/web/routes/signage/screens/form-view-model');
|
||||
|
||||
module.exports = function renderScreenEditPage(screen, data, message, currentUser) {
|
||||
return renderView('screens/form', buildScreenFormViewModel(screen, data || {}, message, currentUser, true));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Screen list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { renderView } = require('#src/web/view');
|
||||
|
||||
module.exports = function renderScreensPage(data, message, currentUser) {
|
||||
return renderView('screens/list', {
|
||||
|
||||
@@ -8,7 +8,7 @@ module.exports = function registerScreensRoutes(app, deps) {
|
||||
const pages = deps.pages;
|
||||
const buildDashboardState = deps.buildDashboardState;
|
||||
const getScreenDeleteBlockMessage = deps.getScreenDeleteBlockMessage;
|
||||
const { buildPagination } = require('../../../lib/pagination');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
const LIST_PAGE_SIZE = 25;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Slide add page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildSlideFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildSlideFormViewModel } = require('#src/web/routes/signage/slides/form-view-model');
|
||||
|
||||
module.exports = function renderSlideAddPage(slideOrData, dataOrMessage, messageOrCurrentUser, currentUserMaybe) {
|
||||
const hasSlideArgument = arguments.length >= 4;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Slide edit page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildSlideFormViewModel } = require('./form-view-model');
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { buildSlideFormViewModel } = require('#src/web/routes/signage/slides/form-view-model');
|
||||
|
||||
module.exports = function renderSlideEditPage(data, slide, message, currentUser) {
|
||||
return renderView('slides/form', buildSlideFormViewModel(data || {}, slide || null, message, currentUser, true));
|
||||
|
||||
Loaded 100 of 174 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user