Release v2.2.0
This commit is contained in:
+5
-2
@@ -1,3 +1,5 @@
|
||||
// Admin data aggregation helpers for dashboards, playlists, slides, templates, and screens.
|
||||
|
||||
const { fetchPagedRows } = require('./utils');
|
||||
|
||||
async function fetchAdminData(pool) {
|
||||
@@ -10,7 +12,7 @@ async function fetchAdminData(pool) {
|
||||
LEFT JOIN c_canvas_sizes cs ON cs.id = st.canvas_size_id
|
||||
ORDER BY st.id DESC
|
||||
`);
|
||||
const [templateRegions] = await pool.query('SELECT id, template_id, region_key, region_type, label, font_family, lock_ratio, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions ORDER BY template_id ASC, z_index ASC, id ASC');
|
||||
const [templateRegions] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions ORDER BY template_id ASC, z_index ASC, id ASC');
|
||||
const [slides] = await pool.query(`
|
||||
SELECT s.id, s.title, s.template_id, s.content_json, s.thumbnail_path, s.created_at, s.modified_at, s.created_by, s.modified_by, st.name AS template_name, cs.width AS canvas_width, cs.height AS canvas_height,
|
||||
(SELECT COUNT(DISTINCT ps.playlist_id) FROM c_playlist_slides ps WHERE ps.slide_id = s.id) AS playlist_count
|
||||
@@ -39,7 +41,8 @@ async function fetchAdminData(pool) {
|
||||
async function fetchPlaylistsPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) {
|
||||
const paged = await fetchPagedRows(pool, {
|
||||
selectSql: `SELECT p.id, p.name, p.fade_between_slides, p.skip_unavailable_rtmp, p.created_at, p.modified_at, p.created_by, p.modified_by,
|
||||
(SELECT COUNT(*) FROM c_playlist_slides ps WHERE ps.playlist_id = p.id) AS slide_count
|
||||
(SELECT COUNT(*) FROM c_playlist_slides ps WHERE ps.playlist_id = p.id) AS slide_count,
|
||||
(SELECT COUNT(*) FROM d_screens s WHERE s.playlist_id = p.id) AS screen_count
|
||||
FROM c_playlists p
|
||||
ORDER BY p.id DESC`,
|
||||
countSql: 'SELECT COUNT(*) AS count FROM c_playlists',
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const ANNOUNCEMENT_ICON_OPTIONS = [
|
||||
{ value: 'megaphone-fill', label: 'Megaphone' },
|
||||
{ value: 'megaphone', label: 'Megaphone outline' },
|
||||
{ value: 'bell-fill', label: 'Bell' },
|
||||
{ value: 'bell', label: 'Bell outline' },
|
||||
{ value: 'exclamation-triangle-fill', label: 'Warning' },
|
||||
{ value: 'exclamation-triangle', label: 'Warning outline' },
|
||||
{ value: 'info-circle-fill', label: 'Info' },
|
||||
{ value: 'info-circle', label: 'Info outline' },
|
||||
{ value: 'check-circle-fill', label: 'Success' },
|
||||
{ value: 'check-circle', label: 'Success outline' },
|
||||
{ value: 'lightbulb-fill', label: 'Idea' },
|
||||
{ value: 'lightbulb', label: 'Idea outline' },
|
||||
{ value: 'calendar-event-fill', label: 'Calendar' },
|
||||
{ value: 'calendar-event', label: 'Calendar outline' },
|
||||
{ value: 'clock-fill', label: 'Clock' },
|
||||
{ value: 'clock', label: 'Clock outline' },
|
||||
{ value: 'wifi-off', label: 'Wi-Fi Offline' },
|
||||
{ value: 'wifi', label: 'Wi-Fi' },
|
||||
{ value: 'hdd-network', label: 'Network' },
|
||||
{ value: 'hdd-network-fill', label: 'Network fill' },
|
||||
{ value: 'speaker-fill', label: 'Speaker' },
|
||||
{ value: 'speaker', label: 'Speaker outline' },
|
||||
{ value: 'shield-fill', label: 'Shield' },
|
||||
{ value: 'shield', label: 'Shield outline' },
|
||||
{ value: 'collection-play-fill', label: 'Playlist' },
|
||||
{ value: 'collection-play', label: 'Playlist outline' },
|
||||
{ value: 'broadcast', label: 'Broadcast' },
|
||||
{ value: 'broadcast-pin', label: 'Broadcast pin' },
|
||||
{ value: 'plug-fill', label: 'Plug' },
|
||||
{ value: 'plug', label: 'Plug outline' },
|
||||
{ value: 'lightning-charge-fill', label: 'Urgent' },
|
||||
{ value: 'lightning-charge', label: 'Urgent outline' },
|
||||
{ value: 'car-front-fill', label: 'Car Front' },
|
||||
{ value: 'car-front', label: 'Car Front outline' },
|
||||
{ value: 'lamp-fill', label: 'Lamp' },
|
||||
{ value: 'lamp', label: 'Lamp outline' },
|
||||
{ value: 'envelope-fill', label: 'Message' },
|
||||
{ value: 'envelope', label: 'Message outline' },
|
||||
{ value: 'people-fill', label: 'Audience' },
|
||||
{ value: 'people', label: 'Audience outline' },
|
||||
{ value: 'browser-chrome', label: 'Browser Chrome' },
|
||||
{ value: 'browser-edge', label: 'Browser Edge' },
|
||||
{ value: 'browser-firefox', label: 'Browser Firefox' },
|
||||
{ value: 'browser-safari', label: 'Browser Safari' },
|
||||
{ value: 'cone', label: 'Cone' },
|
||||
{ value: 'cone-striped', label: 'Cone striped' },
|
||||
{ value: 'cup-straw', label: 'Cup straw' },
|
||||
{ value: 'fire', label: 'Fire' }
|
||||
];
|
||||
|
||||
const ANNOUNCEMENT_ICON_KEYS = ANNOUNCEMENT_ICON_OPTIONS.map(function (option) {
|
||||
return option.value;
|
||||
});
|
||||
|
||||
const ANNOUNCEMENT_ICON_LABELS = ANNOUNCEMENT_ICON_OPTIONS.reduce(function (labels, option) {
|
||||
labels[option.value] = option.label;
|
||||
return labels;
|
||||
}, Object.create(null));
|
||||
|
||||
const DEFAULT_ANNOUNCEMENT_ICON = 'megaphone-fill';
|
||||
|
||||
function normalizeAnnouncementIcon(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return ANNOUNCEMENT_ICON_KEYS.includes(normalized) ? normalized : DEFAULT_ANNOUNCEMENT_ICON;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ANNOUNCEMENT_ICON_OPTIONS,
|
||||
ANNOUNCEMENT_ICON_KEYS,
|
||||
ANNOUNCEMENT_ICON_LABELS,
|
||||
DEFAULT_ANNOUNCEMENT_ICON,
|
||||
normalizeAnnouncementIcon
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
// Announcement data access helpers.
|
||||
|
||||
const { fetchPagedRows } = require('./utils');
|
||||
const {
|
||||
ANNOUNCEMENT_ICON_KEYS,
|
||||
DEFAULT_ANNOUNCEMENT_ICON,
|
||||
normalizeAnnouncementIcon: normalizeAnnouncementIconFromConfig
|
||||
} = require('./announcement-icons');
|
||||
|
||||
const ANNOUNCEMENT_TYPES = ['lower-third', 'fullscreen', 'top-banner'];
|
||||
const ANNOUNCEMENT_COLORS = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'dark', 'light'];
|
||||
|
||||
function normalizeAnnouncementType(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'center-card') {
|
||||
return 'fullscreen';
|
||||
}
|
||||
return ANNOUNCEMENT_TYPES.includes(normalized) ? normalized : 'lower-third';
|
||||
}
|
||||
|
||||
function normalizeAnnouncementColor(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return ANNOUNCEMENT_COLORS.includes(normalized) ? normalized : 'primary';
|
||||
}
|
||||
|
||||
function normalizeAnnouncementMessage(value) {
|
||||
return String(value || '').replace(/\r\n/g, '\n').trim();
|
||||
}
|
||||
|
||||
function normalizeAnnouncementShortLabel(value) {
|
||||
return String(value || '').replace(/\r\n/g, ' ').trim();
|
||||
}
|
||||
|
||||
function normalizeAnnouncementDurationSeconds(value) {
|
||||
const seconds = Math.max(0, Math.round(Number(value) || 0));
|
||||
return seconds > 0 ? seconds : null;
|
||||
}
|
||||
|
||||
function normalizeAnnouncementScreenIds(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(function (screenId) {
|
||||
return Number(screenId) || 0;
|
||||
}).filter(function (screenId) {
|
||||
return screenId > 0;
|
||||
});
|
||||
}
|
||||
|
||||
return String(value || '')
|
||||
.split(',')
|
||||
.map(function (screenId) {
|
||||
return Number(screenId) || 0;
|
||||
})
|
||||
.filter(function (screenId) {
|
||||
return screenId > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function buildAnnouncementPayload(input) {
|
||||
const message = normalizeAnnouncementMessage(input && input.message);
|
||||
const shortLabel = normalizeAnnouncementShortLabel(input && input.short_label);
|
||||
const announcementType = normalizeAnnouncementType(input && input.announcement_type);
|
||||
const colorKey = normalizeAnnouncementColor(input && input.color_key);
|
||||
const iconKey = normalizeAnnouncementIconFromConfig(input && input.icon_key);
|
||||
const durationSeconds = normalizeAnnouncementDurationSeconds(input && input.duration_seconds);
|
||||
|
||||
return {
|
||||
message,
|
||||
shortLabel,
|
||||
announcementType,
|
||||
colorKey,
|
||||
iconKey,
|
||||
durationSeconds,
|
||||
expiresAt: durationSeconds ? new Date(Date.now() + (durationSeconds * 1000)) : null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAnnouncementRow(row) {
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: Number(row.id) || null,
|
||||
message: normalizeAnnouncementMessage(row.message),
|
||||
short_label: normalizeAnnouncementShortLabel(row.short_label),
|
||||
announcement_type: normalizeAnnouncementType(row.announcement_type),
|
||||
color_key: normalizeAnnouncementColor(row.color_key),
|
||||
icon_key: normalizeAnnouncementIconFromConfig(row.icon_key),
|
||||
duration_seconds: row.duration_seconds === null || row.duration_seconds === undefined
|
||||
? null
|
||||
: Math.max(0, Math.round(Number(row.duration_seconds) || 0)),
|
||||
expires_at: row.expires_at || null,
|
||||
screen_target_count: row.screen_target_count === null || row.screen_target_count === undefined
|
||||
? null
|
||||
: Math.max(0, Math.round(Number(row.screen_target_count) || 0)),
|
||||
total_screen_count: row.total_screen_count === null || row.total_screen_count === undefined
|
||||
? null
|
||||
: Math.max(0, Math.round(Number(row.total_screen_count) || 0)),
|
||||
screen_targets_label: String(row.screen_targets_label || '').trim(),
|
||||
screen_ids: normalizeAnnouncementScreenIds(row.screen_ids),
|
||||
created_at: row.created_at || null,
|
||||
modified_at: row.modified_at || null,
|
||||
created_by: row.created_by || null,
|
||||
modified_by: row.modified_by || null
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchAnnouncementById(pool, id) {
|
||||
const [rows] = await pool.query(`
|
||||
SELECT a.id, a.message, a.short_label, a.announcement_type, a.color_key, a.icon_key, a.duration_seconds, a.expires_at, a.created_at, a.modified_at, a.created_by, a.modified_by,
|
||||
GROUP_CONCAT(DISTINCT aas.screen_id ORDER BY aas.screen_id SEPARATOR ',') AS screen_ids
|
||||
FROM d_announcements a
|
||||
LEFT JOIN d_announcement_screens aas ON aas.announcement_id = a.id
|
||||
WHERE a.id = ?
|
||||
GROUP BY a.id
|
||||
`, [id]);
|
||||
return normalizeAnnouncementRow(rows[0] || null);
|
||||
}
|
||||
|
||||
async function fetchAnnouncementsPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) {
|
||||
const paged = await fetchPagedRows(pool, {
|
||||
selectSql: `SELECT a.id, a.message, a.short_label, a.announcement_type, a.color_key, a.icon_key, a.duration_seconds, a.expires_at, a.created_at, a.modified_at, a.created_by, a.modified_by,
|
||||
COUNT(DISTINCT aas.screen_id) AS screen_target_count,
|
||||
(SELECT COUNT(*) FROM d_screens) AS total_screen_count,
|
||||
COALESCE(
|
||||
GROUP_CONCAT(DISTINCT COALESCE(NULLIF(TRIM(s.name), ''), s.slug) ORDER BY COALESCE(NULLIF(TRIM(s.name), ''), s.slug) SEPARATOR ', '),
|
||||
''
|
||||
) AS screen_targets_label
|
||||
FROM d_announcements a
|
||||
LEFT JOIN d_announcement_screens aas ON aas.announcement_id = a.id
|
||||
LEFT JOIN d_screens s ON s.id = aas.screen_id
|
||||
GROUP BY a.id`,
|
||||
countSql: 'SELECT COUNT(*) AS count FROM d_announcements',
|
||||
searchColumns: ['message', 'short_label', 'announcement_type', 'color_key'],
|
||||
searchTerm: searchTerm,
|
||||
sortColumns: {
|
||||
description: 'short_label',
|
||||
message: 'message',
|
||||
type: 'announcement_type',
|
||||
color: 'color_key',
|
||||
status: 'expires_at',
|
||||
expires: 'expires_at',
|
||||
created: 'created_at',
|
||||
modified: 'modified_at'
|
||||
},
|
||||
sortKey: sortKey,
|
||||
sortDirection: sortDirection,
|
||||
page: page,
|
||||
pageSize: pageSize
|
||||
});
|
||||
|
||||
return Object.assign({ announcements: (paged.rows || []).map(normalizeAnnouncementRow) }, paged);
|
||||
}
|
||||
|
||||
async function fetchActiveAnnouncement(pool, screenSlug) {
|
||||
const [rows] = await pool.query(`
|
||||
SELECT a.id, a.message, a.short_label, a.announcement_type, a.color_key, a.icon_key, a.duration_seconds, a.expires_at, a.created_at, a.modified_at, a.created_by, a.modified_by,
|
||||
GROUP_CONCAT(DISTINCT aas.screen_id ORDER BY aas.screen_id SEPARATOR ',') AS screen_ids
|
||||
FROM d_announcements a
|
||||
LEFT JOIN d_announcement_screens aas ON aas.announcement_id = a.id
|
||||
WHERE (a.expires_at IS NULL OR a.expires_at > CURRENT_TIMESTAMP)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM d_announcement_screens restriction
|
||||
JOIN d_screens screen ON screen.id = restriction.screen_id
|
||||
WHERE restriction.announcement_id = a.id
|
||||
AND screen.slug = ?
|
||||
)
|
||||
GROUP BY a.id
|
||||
ORDER BY a.modified_at DESC, a.created_at DESC, a.id DESC
|
||||
LIMIT 1
|
||||
`, [screenSlug]);
|
||||
return normalizeAnnouncementRow(rows[0] || null);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ANNOUNCEMENT_TYPES,
|
||||
ANNOUNCEMENT_COLORS,
|
||||
ANNOUNCEMENT_ICONS: ANNOUNCEMENT_ICON_KEYS,
|
||||
DEFAULT_ANNOUNCEMENT_ICON,
|
||||
normalizeAnnouncementType,
|
||||
normalizeAnnouncementColor,
|
||||
normalizeAnnouncementIcon: normalizeAnnouncementIconFromConfig,
|
||||
normalizeAnnouncementShortLabel,
|
||||
buildAnnouncementPayload,
|
||||
fetchAnnouncementById,
|
||||
fetchAnnouncementsPage,
|
||||
fetchActiveAnnouncement
|
||||
};
|
||||
+96
-14
@@ -1,3 +1,5 @@
|
||||
// API source data access, pagination, and remote fetch helpers.
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const { fetchPagedRows } = require('./utils');
|
||||
@@ -7,9 +9,44 @@ function normalizeUpdateIntervalUnit(value) {
|
||||
return unit === 'seconds' ? 'seconds' : 'minutes';
|
||||
}
|
||||
|
||||
function normalizeAuthMethod(value) {
|
||||
const method = String(value || '').trim().toLowerCase();
|
||||
return ['basic', 'bearer', 'api_key_header'].includes(method) ? method : 'none';
|
||||
}
|
||||
|
||||
function getItemsPath(source) {
|
||||
return String(source && (source.items_path || source.itemsPath) || '').trim();
|
||||
}
|
||||
|
||||
function buildAuthHeaders(source) {
|
||||
const method = normalizeAuthMethod(source && (source.auth_method || source.authMethod));
|
||||
const headers = {};
|
||||
|
||||
if (method === 'basic') {
|
||||
const username = String(source && (source.auth_username || source.authUsername) || '').trim();
|
||||
const password = String(source && (source.auth_password || source.authPassword) || '');
|
||||
if (username || password) {
|
||||
headers.Authorization = 'Basic ' + Buffer.from(username + ':' + password, 'utf8').toString('base64');
|
||||
}
|
||||
} else if (method === 'bearer') {
|
||||
const token = String(source && (source.auth_bearer_token || source.authBearerToken) || '').trim();
|
||||
if (token) {
|
||||
headers.Authorization = 'Bearer ' + token;
|
||||
}
|
||||
} else if (method === 'api_key_header') {
|
||||
const headerName = String(source && (source.auth_header_name || source.authHeaderName) || 'X-API-Key').trim() || 'X-API-Key';
|
||||
const headerValue = String(source && (source.auth_header_value || source.authHeaderValue) || '').trim();
|
||||
if (headerValue) {
|
||||
headers[headerName] = headerValue;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function fetchApiSourcesData(pool) {
|
||||
const [apiSources] = await pool.query(
|
||||
'SELECT id, name, api_url, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources ORDER BY modified_at DESC, id DESC'
|
||||
'SELECT id, name, api_url, auth_method, auth_username, auth_password, auth_bearer_token, auth_header_name, auth_header_value, items_path, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources ORDER BY modified_at DESC, id DESC'
|
||||
);
|
||||
|
||||
return { apiSources: apiSources };
|
||||
@@ -17,7 +54,7 @@ async function fetchApiSourcesData(pool) {
|
||||
|
||||
async function fetchApiSourcesPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) {
|
||||
const paged = await fetchPagedRows(pool, {
|
||||
selectSql: 'SELECT id, name, api_url, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources ORDER BY modified_at DESC, id DESC',
|
||||
selectSql: 'SELECT id, name, api_url, auth_method, auth_username, auth_password, auth_bearer_token, auth_header_name, auth_header_value, items_path, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources ORDER BY modified_at DESC, id DESC',
|
||||
countSql: 'SELECT COUNT(*) AS count FROM i_api_sources',
|
||||
searchColumns: ['name', 'api_url', 'last_pull_error'],
|
||||
searchTerm: searchTerm,
|
||||
@@ -41,20 +78,21 @@ async function fetchApiSourcesPage(pool, page, pageSize, searchTerm, sortKey, so
|
||||
|
||||
async function fetchApiSourceById(pool, id) {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, name, api_url, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources WHERE id = ?',
|
||||
'SELECT id, name, api_url, auth_method, auth_username, auth_password, auth_bearer_token, auth_header_name, auth_header_value, items_path, update_interval_value, update_interval_unit, last_pulled_at, last_pull_error, last_response_status, last_response_content_type, last_response_json, created_at, modified_at, created_by, modified_by FROM i_api_sources WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function loadUrlText(urlValue) {
|
||||
async function loadUrlText(urlValue, requestOptions) {
|
||||
const extraHeaders = requestOptions && requestOptions.headers ? requestOptions.headers : {};
|
||||
if (typeof fetch === 'function') {
|
||||
const response = await fetch(urlValue, {
|
||||
headers: {
|
||||
headers: Object.assign({
|
||||
Accept: 'application/json, text/plain;q=0.9, */*;q=0.8',
|
||||
'User-Agent': 'Pulse Signage API Reader'
|
||||
}
|
||||
}, extraHeaders)
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -68,12 +106,13 @@ async function loadUrlText(urlValue) {
|
||||
return await new Promise(function (resolve, reject) {
|
||||
const url = new URL(urlValue);
|
||||
const transport = url.protocol === 'https:' ? https : http;
|
||||
const request = transport.get(url, {
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain;q=0.9, */*;q=0.8',
|
||||
'User-Agent': 'Pulse Signage API Reader'
|
||||
}
|
||||
}, function (response) {
|
||||
const requestHeaders = Object.assign({
|
||||
Accept: 'application/json, text/plain;q=0.9, */*;q=0.8',
|
||||
'User-Agent': 'Pulse Signage API Reader'
|
||||
}, extraHeaders);
|
||||
const request = transport.get(url, Object.assign({}, requestOptions || {}, {
|
||||
headers: requestHeaders
|
||||
}), function (response) {
|
||||
response.setEncoding('utf8');
|
||||
let body = '';
|
||||
response.on('data', function (chunk) {
|
||||
@@ -94,8 +133,11 @@ async function loadUrlText(urlValue) {
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchApiSourceResponse(apiUrl) {
|
||||
const response = await loadUrlText(apiUrl);
|
||||
async function fetchApiSourceResponse(apiSource) {
|
||||
const source = apiSource && typeof apiSource === 'object' ? apiSource : { api_url: apiSource };
|
||||
const response = await loadUrlText(source.api_url, {
|
||||
headers: buildAuthHeaders(source)
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to load API response (${response.statusCode}).`);
|
||||
}
|
||||
@@ -121,8 +163,23 @@ async function fetchApiSourceResponse(apiUrl) {
|
||||
|
||||
function buildApiSourcePayload(req, existingApiSource) {
|
||||
const fallback = existingApiSource || {};
|
||||
const body = req && req.body ? req.body : {};
|
||||
function readBodyValue(fieldName, fallbackValue) {
|
||||
if (Object.prototype.hasOwnProperty.call(body, fieldName)) {
|
||||
return body[fieldName];
|
||||
}
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
const name = String(req.body.name || fallback.name || '').trim();
|
||||
const apiUrl = String(req.body.api_url || req.body.apiUrl || fallback.api_url || '').trim();
|
||||
const authMethod = normalizeAuthMethod(readBodyValue('auth_method', readBodyValue('authMethod', fallback.auth_method || 'none')));
|
||||
const authUsername = String(readBodyValue('auth_username', readBodyValue('authUsername', fallback.auth_username || '')) || '').trim();
|
||||
const authPassword = String(readBodyValue('auth_password', readBodyValue('authPassword', fallback.auth_password || '')) || '');
|
||||
const authBearerToken = String(readBodyValue('auth_bearer_token', readBodyValue('authBearerToken', fallback.auth_bearer_token || '')) || '').trim();
|
||||
const authHeaderName = String(readBodyValue('auth_header_name', readBodyValue('authHeaderName', fallback.auth_header_name || 'X-API-Key')) || 'X-API-Key').trim() || 'X-API-Key';
|
||||
const authHeaderValue = String(readBodyValue('auth_header_value', readBodyValue('authHeaderValue', fallback.auth_header_value || '')) || '').trim();
|
||||
const itemsPath = String(readBodyValue('items_path', readBodyValue('itemsPath', fallback.items_path || '')) || '').trim();
|
||||
const updateIntervalValue = Math.max(1, Number(req.body.update_interval_value || req.body.updateIntervalValue || fallback.update_interval_value || 60));
|
||||
const updateIntervalUnit = normalizeUpdateIntervalUnit(req.body.update_interval_unit || req.body.updateIntervalUnit || fallback.update_interval_unit || 'minutes');
|
||||
|
||||
@@ -159,9 +216,34 @@ function buildApiSourcePayload(req, existingApiSource) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (authMethod === 'basic' && !authUsername && !authPassword) {
|
||||
const error = new Error('Basic auth requires a username or password.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (authMethod === 'bearer' && !authBearerToken) {
|
||||
const error = new Error('Bearer auth requires a token.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (authMethod === 'api_key_header' && !authHeaderValue) {
|
||||
const error = new Error('API key auth requires a header value.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
apiUrl: parsedUrl.toString(),
|
||||
authMethod: authMethod,
|
||||
authUsername: authUsername,
|
||||
authPassword: authPassword,
|
||||
authBearerToken: authBearerToken,
|
||||
authHeaderName: authHeaderName,
|
||||
authHeaderValue: authHeaderValue,
|
||||
itemsPath: itemsPath,
|
||||
updateIntervalValue: Math.floor(updateIntervalValue),
|
||||
updateIntervalUnit: updateIntervalUnit
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Canvas size data access and pagination helpers.
|
||||
|
||||
const { fetchPagedRows } = require('./utils');
|
||||
|
||||
async function fetchCanvasSizesData(pool) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Client name reservation helpers for onboarding and live connection checks.
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
function normalizeClientName(value) {
|
||||
|
||||
+20
-1
@@ -1,8 +1,12 @@
|
||||
// Aggregated exports for the shared data access layer.
|
||||
|
||||
const { fetchAdminData, fetchPlaylistsPage, fetchSlidesPage, fetchTemplatesPage, fetchCanvasSizesPage, fetchScreensPage } = require('./admin');
|
||||
const { ANNOUNCEMENT_TYPES, ANNOUNCEMENT_COLORS, ANNOUNCEMENT_ICONS, DEFAULT_ANNOUNCEMENT_ICON, normalizeAnnouncementType, normalizeAnnouncementColor, normalizeAnnouncementIcon, fetchAnnouncementsPage, fetchAnnouncementById, fetchActiveAnnouncement, buildAnnouncementPayload } = require('./announcements');
|
||||
const { ANNOUNCEMENT_ICON_OPTIONS, ANNOUNCEMENT_ICON_LABELS } = require('./announcement-icons');
|
||||
const { fetchPlaylistById } = require('./playlists');
|
||||
const { fetchApiSourcesData, fetchApiSourcesPage, fetchApiSourceById, fetchApiSourceResponse, buildApiSourcePayload } = require('./api-sources');
|
||||
const { fetchRssFeedsData, fetchRssFeedsPage, fetchRssFeedById, fetchRssFeedItemsByFeedId, normalizeRssFeedItem, buildRssFeedPayload, fetchRssFeedItems, replaceRssFeedItems } = require('./rss-feeds');
|
||||
const { slugify, uniqueScreenSlug, fetchScreenById, fetchScreenEditData } = require('./screens');
|
||||
const { slugify, uniqueScreenSlug, fetchScreenById, fetchScreenEditData, fetchScreenPlayerUrls, fetchScreenPlayerRecord } = require('./screens');
|
||||
const { fetchTemplateById, fetchTemplatesData, extractTemplateRegions, buildTemplatePayload } = require('./templates');
|
||||
const { fetchCanvasSizesData, fetchCanvasSizeById, buildCanvasSizePayload } = require('./canvas-sizes');
|
||||
const { fetchSlideById, buildSlidePayload } = require('./slides');
|
||||
@@ -18,6 +22,19 @@ module.exports = {
|
||||
fetchTemplatesPage,
|
||||
fetchCanvasSizesPage,
|
||||
fetchScreensPage,
|
||||
ANNOUNCEMENT_TYPES,
|
||||
ANNOUNCEMENT_COLORS,
|
||||
ANNOUNCEMENT_ICONS,
|
||||
ANNOUNCEMENT_ICON_OPTIONS,
|
||||
ANNOUNCEMENT_ICON_LABELS,
|
||||
DEFAULT_ANNOUNCEMENT_ICON,
|
||||
normalizeAnnouncementType,
|
||||
normalizeAnnouncementColor,
|
||||
normalizeAnnouncementIcon,
|
||||
fetchAnnouncementsPage,
|
||||
fetchAnnouncementById,
|
||||
fetchActiveAnnouncement,
|
||||
buildAnnouncementPayload,
|
||||
fetchPlaylistById,
|
||||
fetchApiSourcesData,
|
||||
fetchApiSourcesPage,
|
||||
@@ -34,6 +51,8 @@ module.exports = {
|
||||
replaceRssFeedItems,
|
||||
fetchScreenById,
|
||||
fetchScreenEditData,
|
||||
fetchScreenPlayerUrls,
|
||||
fetchScreenPlayerRecord,
|
||||
fetchTemplateById,
|
||||
fetchSlideById,
|
||||
fetchTemplatesData,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Playlist data access helpers.
|
||||
|
||||
async function fetchPlaylistById(pool, id) {
|
||||
const [rows] = await pool.query('SELECT id, name, fade_between_slides, skip_unavailable_rtmp, created_at, modified_at, created_by, modified_by FROM c_playlists WHERE id = ?', [id]);
|
||||
return rows[0] || null;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// RSS feed data access, pagination, and item normalization helpers.
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const { fetchPagedRows } = require('./utils');
|
||||
@@ -50,7 +52,7 @@ async function fetchRssFeedById(pool, id) {
|
||||
async function fetchRssFeedItemsByFeedId(pool, rssFeedId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, rss_feed_id, position, item_json, created_at, modified_at
|
||||
FROM rss_feed_items
|
||||
FROM i_rss_feed_items
|
||||
WHERE rss_feed_id = ?
|
||||
ORDER BY position ASC, id ASC`,
|
||||
[rssFeedId]
|
||||
@@ -229,7 +231,7 @@ async function fetchRssFeedItems(feedUrl, itemLimit) {
|
||||
|
||||
async function replaceRssFeedItems(connection, rssFeedId, items) {
|
||||
const normalizedItems = Array.isArray(items) ? items : [];
|
||||
await connection.query('DELETE FROM rss_feed_items WHERE rss_feed_id = ?', [rssFeedId]);
|
||||
await connection.query('DELETE FROM i_rss_feed_items WHERE rss_feed_id = ?', [rssFeedId]);
|
||||
|
||||
if (!normalizedItems.length) {
|
||||
return 0;
|
||||
@@ -244,7 +246,7 @@ async function replaceRssFeedItems(connection, rssFeedId, items) {
|
||||
});
|
||||
|
||||
await connection.query(
|
||||
'INSERT INTO rss_feed_items (rss_feed_id, position, item_json) VALUES ?',
|
||||
'INSERT INTO i_rss_feed_items (rss_feed_id, position, item_json) VALUES ?',
|
||||
[insertValues]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Screen slug helpers and screen lookup utilities.
|
||||
|
||||
function slugify(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
@@ -7,6 +9,60 @@ function slugify(value) {
|
||||
.replace(/-{2,}/g, '-');
|
||||
}
|
||||
|
||||
function normalizePlayerBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function buildScreenPlayerUrl(screen, fallbackBaseUrl) {
|
||||
const slug = String(screen && screen.slug || '').trim();
|
||||
if (!slug) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseUrl = normalizePlayerBaseUrl(fallbackBaseUrl);
|
||||
if (!baseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${baseUrl}/screen/${encodeURIComponent(slug)}`;
|
||||
}
|
||||
|
||||
async function fetchScreenPlayerUrls(pool) {
|
||||
const [rows] = await pool.query(`
|
||||
SELECT s.slug, p.public_base_url
|
||||
FROM d_screens s
|
||||
LEFT JOIN d_players p ON p.device_id = s.player_id
|
||||
WHERE p.public_base_url IS NOT NULL
|
||||
AND TRIM(p.public_base_url) <> ''
|
||||
ORDER BY p.modified_at DESC, s.slug ASC
|
||||
`);
|
||||
|
||||
const playerUrls = {};
|
||||
rows.forEach(function (row) {
|
||||
const slug = String(row && row.slug || '').trim();
|
||||
const publicBaseUrl = normalizePlayerBaseUrl(row && row.public_base_url);
|
||||
const playerUrl = buildScreenPlayerUrl({ slug: slug }, publicBaseUrl);
|
||||
if (slug && playerUrl && !playerUrls[slug]) {
|
||||
playerUrls[slug] = playerUrl;
|
||||
}
|
||||
});
|
||||
|
||||
return playerUrls;
|
||||
}
|
||||
|
||||
async function fetchScreenPlayerRecord(pool, slug) {
|
||||
const [rows] = await pool.query(`
|
||||
SELECT p.device_id, p.public_base_url, p.internal_base_url, p.last_seen_at
|
||||
FROM d_screens s
|
||||
JOIN d_players p ON p.device_id = s.player_id
|
||||
WHERE s.slug = ?
|
||||
ORDER BY p.modified_at DESC, p.device_id ASC
|
||||
LIMIT 1
|
||||
`, [slug]);
|
||||
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function uniqueScreenSlug(pool, baseSlug, excludeId) {
|
||||
const start = baseSlug || `screen-${Date.now()}`;
|
||||
let candidate = start;
|
||||
@@ -44,6 +100,10 @@ async function fetchScreenEditData(pool) {
|
||||
|
||||
module.exports = {
|
||||
slugify,
|
||||
normalizePlayerBaseUrl,
|
||||
buildScreenPlayerUrl,
|
||||
fetchScreenPlayerUrls,
|
||||
fetchScreenPlayerRecord,
|
||||
uniqueScreenSlug,
|
||||
fetchScreenById,
|
||||
fetchScreenEditData
|
||||
|
||||
+82
-5
@@ -1,3 +1,5 @@
|
||||
// Slide data access helpers, including rich-text normalization and payload building.
|
||||
|
||||
const { fetchTemplateById } = require('./templates');
|
||||
const { parseJsonSafe } = require('./utils');
|
||||
|
||||
@@ -25,6 +27,25 @@ function sanitizeRichText(html) {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizePlainText(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/<\s*br\s*\/?\s*>/gi, '\n')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /gi, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function stripEditorOnlyMarkup(value) {
|
||||
return String(value || '')
|
||||
.replace(/<pre[^>]*class="[^"]*api-region-sample-preview[^"]*"[^>]*>[\s\S]*?<\/pre>/gi, '')
|
||||
.replace(/<details[^>]*class="[^"]*api-region-sample-accordion[^"]*"[^>]*>[\s\S]*?<\/details>/gi, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizeEditorMarkup(value) {
|
||||
return String(value === undefined || value === null ? '' : value).trim();
|
||||
}
|
||||
|
||||
async function fetchSlideById(pool, id) {
|
||||
const [slides] = await pool.query(`
|
||||
SELECT s.id, s.title, s.template_id, s.content_json, s.thumbnail_path, s.created_at, s.modified_at,
|
||||
@@ -53,6 +74,14 @@ function getFilesByField(files) {
|
||||
return map;
|
||||
}
|
||||
|
||||
function getSubmittedValue(body, key, fallback) {
|
||||
if (body && Object.prototype.hasOwnProperty.call(body, key)) {
|
||||
return String(body[key] || '').trim();
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function sanitizeTextColor(value, fallback) {
|
||||
const raw = String(value || '').trim();
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(raw) || /^#[0-9a-fA-F]{3}$/.test(raw)) {
|
||||
@@ -88,9 +117,10 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
|
||||
if (region.region_type === 'image') {
|
||||
const uploaded = filesByField[`region_image_${region.id}`];
|
||||
const existing = body[`existing_region_image_${region.id}`];
|
||||
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key].value : '';
|
||||
content[region.region_key] = {
|
||||
type: 'image',
|
||||
value: uploaded ? `/media/uploads/${uploaded.filename}` : String(existing || '').trim() || ((existingContent && existingContent[region.region_key]) ? existingContent[region.region_key].value : '')
|
||||
value: uploaded ? `/media/uploads/${uploaded.filename}` : getSubmittedValue(body, `existing_region_image_${region.id}`, current)
|
||||
};
|
||||
} else if (region.region_type === 'video') {
|
||||
const uploaded = filesByField[`region_video_${region.id}`];
|
||||
@@ -99,9 +129,10 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
|
||||
const existingDuration = existingContent && existingContent[region.region_key] ? Number(existingContent[region.region_key].duration_seconds || 0) : 0;
|
||||
const parsedDuration = Number(durationValue || existingDuration || 0);
|
||||
const normalizedDuration = Math.round(parsedDuration * 1000) / 1000;
|
||||
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key].value : '';
|
||||
content[region.region_key] = {
|
||||
type: 'video',
|
||||
value: uploaded ? `/media/uploads/${uploaded.filename}` : String(existing || '').trim() || ((existingContent && existingContent[region.region_key]) ? existingContent[region.region_key].value : ''),
|
||||
value: uploaded ? `/media/uploads/${uploaded.filename}` : getSubmittedValue(body, `existing_region_video_${region.id}`, current),
|
||||
duration_seconds: Number.isFinite(normalizedDuration) && normalizedDuration > 0 ? normalizedDuration : null
|
||||
};
|
||||
} else if (region.region_type === 'webpage') {
|
||||
@@ -126,6 +157,15 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
|
||||
type: 'html',
|
||||
value: submitted === undefined ? current : String(submitted || '')
|
||||
};
|
||||
} else if (region.region_type === 'time-date') {
|
||||
const submitted = body[`region_text_${region.id}`];
|
||||
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||
const timezoneValue = body[`region_timezone_${region.id}`];
|
||||
content[region.region_key] = {
|
||||
type: 'time-date',
|
||||
value: submitted === undefined ? String(current.value !== undefined ? current.value : current.text !== undefined ? current.text : '') : String(submitted || ''),
|
||||
timezone: timezoneValue === undefined || timezoneValue === null ? String(current.timezone || current.time_zone || '') : String(timezoneValue || '').trim()
|
||||
};
|
||||
} else if (region.region_type === 'rss') {
|
||||
const submitted = body[`region_text_${region.id}`];
|
||||
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||
@@ -135,7 +175,7 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
|
||||
const parsedItemNumber = Math.max(1, Number(itemNumber || current.item_number || 1));
|
||||
content[region.region_key] = {
|
||||
type: 'rss',
|
||||
value: submitted === undefined ? String(current.value || '') : String(submitted || ''),
|
||||
value: stripEditorOnlyMarkup(normalizeEditorMarkup(submitted === undefined ? String(current.value || '') : String(submitted || ''))),
|
||||
feed_id: feedId === undefined || feedId === null || feedId === '' ? (current.feed_id || null) : Number(feedId),
|
||||
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
|
||||
variable_name: 'item',
|
||||
@@ -149,24 +189,61 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
|
||||
const style = getTextRegionStyle(body, region, existingContent);
|
||||
const sourceId = body[`region_api_source_id_${region.id}`];
|
||||
const itemNumber = body[`region_api_item_number_${region.id}`];
|
||||
const itemsPath = body[`region_api_items_path_${region.id}`];
|
||||
const parsedItemNumber = Math.max(1, Number(itemNumber || current.item_number || 1));
|
||||
content[region.region_key] = {
|
||||
type: 'api',
|
||||
value: submitted === undefined ? String(current.value || '') : String(submitted || ''),
|
||||
value: stripEditorOnlyMarkup(normalizeEditorMarkup(submitted === undefined ? String(current.value || '') : String(submitted || ''))),
|
||||
source_id: sourceId === undefined || sourceId === null || sourceId === '' ? (current.source_id || null) : Number(sourceId),
|
||||
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
|
||||
items_path: itemsPath === undefined || itemsPath === null ? (current.items_path === undefined || current.items_path === null ? '' : String(current.items_path)) : String(itemsPath || '').trim(),
|
||||
variable_name: 'item',
|
||||
font_family: style.font_family,
|
||||
font_size: style.font_size,
|
||||
font_color: style.font_color
|
||||
};
|
||||
} else if (!['text', 'image', 'video', 'webpage', 'html', 'rtmp', 'rss', 'api'].includes(String(region.region_type || '').trim())) {
|
||||
const current = existingContent && existingContent[region.region_key] && typeof existingContent[region.region_key] === 'object' ? existingContent[region.region_key] : {};
|
||||
const suffix = '_' + region.id;
|
||||
const generic = {};
|
||||
|
||||
Object.keys(body || {}).forEach((key) => {
|
||||
if (!key.startsWith('region_') || !key.endsWith(suffix)) {
|
||||
return;
|
||||
}
|
||||
const field = key.slice('region_'.length, -suffix.length);
|
||||
if (!field || field === 'type' || field === 'key' || field === 'name' || field === 'label') {
|
||||
return;
|
||||
}
|
||||
generic[field] = body[key];
|
||||
});
|
||||
|
||||
Object.keys(filesByField || {}).forEach((fieldName) => {
|
||||
if (!fieldName.startsWith('region_') || !fieldName.endsWith(suffix)) {
|
||||
return;
|
||||
}
|
||||
const field = fieldName.slice('region_'.length, -suffix.length);
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
generic[field] = `/media/uploads/${filesByField[fieldName].filename}`;
|
||||
});
|
||||
|
||||
Object.keys(current).forEach((key) => {
|
||||
if (generic[key] === undefined) {
|
||||
generic[key] = current[key];
|
||||
}
|
||||
});
|
||||
|
||||
generic.type = region.region_type;
|
||||
content[region.region_key] = generic;
|
||||
} else {
|
||||
const submitted = body[`region_text_${region.id}`];
|
||||
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key].value : '';
|
||||
const style = getTextRegionStyle(body, region, existingContent);
|
||||
content[region.region_key] = {
|
||||
type: 'text',
|
||||
value: submitted === undefined ? current : String(submitted),
|
||||
value: stripEditorOnlyMarkup(normalizeEditorMarkup(submitted === undefined ? current : String(submitted || ''))),
|
||||
font_family: style.font_family,
|
||||
font_size: style.font_size,
|
||||
font_color: style.font_color
|
||||
|
||||
+42
-9
@@ -1,7 +1,6 @@
|
||||
const { parseJsonSafe, readFormArray } = require('./utils');
|
||||
// Template data access helpers and region normalization logic.
|
||||
|
||||
const ALLOWED_TEMPLATE_REGION_TYPES = ['text', 'image', 'video', 'webpage', 'html', 'rtmp', 'rss', 'api'];
|
||||
const FONT_FAMILY_REGION_TYPES = ['text', 'html', 'rss', 'api'];
|
||||
const { parseJsonSafe, readFormArray } = require('./utils');
|
||||
|
||||
function sanitizeBackgroundColor(value) {
|
||||
const raw = String(value || '').trim();
|
||||
@@ -13,7 +12,7 @@ function sanitizeBackgroundColor(value) {
|
||||
|
||||
function normalizeTemplateRegionType(value) {
|
||||
const rawType = String(value || 'text').trim();
|
||||
return ALLOWED_TEMPLATE_REGION_TYPES.includes(rawType) ? rawType : 'text';
|
||||
return rawType || 'text';
|
||||
}
|
||||
|
||||
function normalizeTemplateRegionLockRatio(value) {
|
||||
@@ -58,7 +57,7 @@ async function fetchTemplateById(pool, id) {
|
||||
return null;
|
||||
}
|
||||
const template = templates[0];
|
||||
const [regions] = await pool.query('SELECT id, template_id, region_key, region_type, label, font_family, lock_ratio, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions WHERE template_id = ? ORDER BY z_index ASC, id ASC', [id]);
|
||||
const [regions] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions WHERE template_id = ? ORDER BY z_index ASC, id ASC', [id]);
|
||||
template.regions = regions;
|
||||
return template;
|
||||
}
|
||||
@@ -71,7 +70,7 @@ async function fetchTemplatesData(pool) {
|
||||
LEFT JOIN c_canvas_sizes cs ON cs.id = st.canvas_size_id
|
||||
ORDER BY st.id DESC
|
||||
`);
|
||||
const [templateRegions] = await pool.query('SELECT id, template_id, region_key, region_type, label, font_family, lock_ratio, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions ORDER BY template_id ASC, z_index ASC, id ASC');
|
||||
const [templateRegions] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions ORDER BY template_id ASC, z_index ASC, id ASC');
|
||||
return { templates, templateRegions };
|
||||
}
|
||||
|
||||
@@ -86,7 +85,6 @@ function extractTemplateRegions(body) {
|
||||
region_key: String(region.region_name || region.region_key || region.label || '').trim(),
|
||||
region_type: regionType,
|
||||
label: String(region.region_name || region.label || region.region_key || '').trim(),
|
||||
font_family: FONT_FAMILY_REGION_TYPES.includes(regionType) ? String(region.font_family || '').trim() || null : null,
|
||||
lock_ratio: normalizeTemplateRegionLockRatio(region.lock_ratio),
|
||||
x: Number(region.x || 0),
|
||||
y: Number(region.y || 0),
|
||||
@@ -108,7 +106,6 @@ function extractTemplateRegions(body) {
|
||||
const widths = readFormArray(body, 'region_width[]');
|
||||
const heights = readFormArray(body, 'region_height[]');
|
||||
const zs = readFormArray(body, 'region_z[]');
|
||||
const fonts = readFormArray(body, 'font_family[]');
|
||||
const regions = [];
|
||||
|
||||
for (let i = 0; i < keys.length; i += 1) {
|
||||
@@ -122,7 +119,6 @@ function extractTemplateRegions(body) {
|
||||
region_key: name,
|
||||
region_type: regionType,
|
||||
label: name,
|
||||
font_family: FONT_FAMILY_REGION_TYPES.includes(regionType) ? String(fonts[i] || 'Arial').trim() || 'Arial' : null,
|
||||
lock_ratio: normalizeTemplateRegionLockRatio(ratios[i]),
|
||||
x: Number(xs[i] || 0),
|
||||
y: Number(ys[i] || 0),
|
||||
@@ -135,6 +131,43 @@ function extractTemplateRegions(body) {
|
||||
return regions;
|
||||
}
|
||||
|
||||
function extractGenericRegionContent(region, body, filesByField, existingContent) {
|
||||
const content = {};
|
||||
const current = existingContent && existingContent[region.region_key] && typeof existingContent[region.region_key] === 'object' ? existingContent[region.region_key] : {};
|
||||
const suffix = '_' + region.id;
|
||||
|
||||
Object.keys(body || {}).forEach((key) => {
|
||||
if (!key.startsWith('region_') || !key.endsWith(suffix)) {
|
||||
return;
|
||||
}
|
||||
const field = key.slice('region_'.length, -suffix.length);
|
||||
if (!field || field === 'type' || field === 'key' || field === 'name' || field === 'label') {
|
||||
return;
|
||||
}
|
||||
content[field] = body[key];
|
||||
});
|
||||
|
||||
Object.keys(filesByField || {}).forEach((fieldName) => {
|
||||
if (!fieldName.startsWith('region_') || !fieldName.endsWith(suffix)) {
|
||||
return;
|
||||
}
|
||||
const field = fieldName.slice('region_'.length, -suffix.length);
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
content[field] = `/media/uploads/${filesByField[fieldName].filename}`;
|
||||
});
|
||||
|
||||
Object.keys(current).forEach((key) => {
|
||||
if (content[key] === undefined) {
|
||||
content[key] = current[key];
|
||||
}
|
||||
});
|
||||
|
||||
content.type = region.region_type;
|
||||
return content;
|
||||
}
|
||||
|
||||
function getFilesByField(files) {
|
||||
const map = {};
|
||||
(files || []).forEach((file) => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Shared helpers for parsing JSON, reading form arrays, and duplicate-name checks.
|
||||
|
||||
function parseJsonSafe(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user