Release 1.5.3

This commit is contained in:
2026-07-25 15:11:41 +01:00
parent 9ee938fd2f
commit 0e89892c94
89 changed files with 7660 additions and 542 deletions
+46 -6
View File
@@ -13,15 +13,37 @@ module.exports = function registerAdminContentRoutes(app, deps) {
const redirectAfterSave = deps.redirectAfterSave;
const notifyPlayerScreens = deps.notifyPlayerScreens;
const broadcastDashboardState = deps.broadcastDashboardState;
const backgroundTaskQueue = deps.backgroundTaskQueue;
const getSlideDeleteBlockMessage = deps.getSlideDeleteBlockMessage;
const getTemplateDeleteBlockMessage = deps.getTemplateDeleteBlockMessage;
const getCanvasSizeDeleteBlockMessage = deps.getCanvasSizeDeleteBlockMessage;
const requirePermission = deps.requirePermission;
const { buildPagination } = require('../../lib/pagination');
const LIST_PAGE_SIZE = 10;
if (!pool || !common || !pages || !upload || typeof fetchScreensBySlideId !== 'function' || typeof fetchScreensByTemplateId !== 'function' || typeof collectUploadReferencesFromSlide !== 'function' || typeof collectUploadReferencesFromTemplate !== 'function' || typeof collectUploadReferencesFromPayload !== 'function' || typeof syncPlaylistUploadsOnChange !== 'function' || typeof getAuditUserId !== 'function' || typeof redirectAfterSave !== 'function' || typeof notifyPlayerScreens !== 'function' || typeof broadcastDashboardState !== 'function' || typeof getSlideDeleteBlockMessage !== 'function' || typeof getTemplateDeleteBlockMessage !== 'function' || typeof getCanvasSizeDeleteBlockMessage !== 'function') {
throw new Error('registerAdminContentRoutes requires the content route dependencies.');
}
function queueSlideThumbnailRefresh(slideId, previousThumbnailPath) {
if (!backgroundTaskQueue || typeof backgroundTaskQueue.enqueueTask !== 'function') {
return Promise.resolve(null);
}
return backgroundTaskQueue.enqueueTask({
key: 'slide-thumbnail:' + Number(slideId),
title: 'Refresh slide thumbnail',
category: 'slides',
taskType: 'slide-thumbnail-refresh',
payload: {
slideId: Number(slideId),
previousThumbnailPath: String(previousThumbnailPath || '').trim()
},
persist: true
});
}
async function canvasSizeExists(width, height, ignoreId) {
const params = [width, height];
let query = 'SELECT COUNT(*) AS count FROM canvas_sizes WHERE width = ? AND height = ?';
@@ -58,8 +80,12 @@ module.exports = function registerAdminContentRoutes(app, deps) {
app.get('/slides', requirePermission('slides.read'), async function (req, res, next) {
try {
const data = await common.fetchAdminData(pool);
res.send(pages.renderSlidesPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await common.fetchSlidesPage(pool, page, LIST_PAGE_SIZE);
res.send(pages.renderSlidesPage({
slides: data.slides || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'slides', 'Slide pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
@@ -136,6 +162,9 @@ module.exports = function registerAdminContentRoutes(app, deps) {
localUploadDir: deps.uploadDir,
nextUploadRefs: collectUploadReferencesFromPayload(payload)
});
queueSlideThumbnailRefresh(result.insertId, null).catch(function (error) {
console.warn('Unable to queue slide thumbnail refresh:', error);
});
redirectAfterSave(req, res, '/slides/' + result.insertId + '/edit', {
closeUrl: '/slides',
newUrl: '/slides/new',
@@ -176,6 +205,9 @@ module.exports = function registerAdminContentRoutes(app, deps) {
screenSlideCounts: screenSlideCounts
});
await broadcastDashboardState();
queueSlideThumbnailRefresh(slide.id, slide.thumbnail_path).catch(function (error) {
console.warn('Unable to queue slide thumbnail refresh:', error);
});
redirectAfterSave(req, res, '/slides/' + slide.id + '/edit', {
closeUrl: '/slides',
newUrl: '/slides/new',
@@ -218,8 +250,12 @@ module.exports = function registerAdminContentRoutes(app, deps) {
app.get('/templates', requirePermission('templates.read'), async function (req, res, next) {
try {
const data = await common.fetchTemplatesData(pool);
res.send(pages.renderTemplatesPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await common.fetchTemplatesPage(pool, page, LIST_PAGE_SIZE);
res.send(pages.renderTemplatesPage({
templates: data.templates || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'templates', 'Template pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
@@ -355,8 +391,12 @@ module.exports = function registerAdminContentRoutes(app, deps) {
app.get('/canvas-sizes', requirePermission('canvas-sizes.read'), async function (req, res, next) {
try {
const data = await common.fetchAdminData(pool);
res.send(pages.renderCanvasSizesPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await common.fetchCanvasSizesPage(pool, page, LIST_PAGE_SIZE);
res.send(pages.renderCanvasSizesPage({
canvasSizes: data.canvasSizes || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'canvas sizes', 'Canvas size pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
+24 -4
View File
@@ -13,6 +13,9 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
const fetchRssFeedItems = deps.fetchRssFeedItems || (common && common.fetchRssFeedItems) || null;
const replaceRssFeedItems = deps.replaceRssFeedItems || (common && common.replaceRssFeedItems) || null;
const fetchApiSourceResponse = common && common.fetchApiSourceResponse ? common.fetchApiSourceResponse : null;
const { buildPagination } = require('../../lib/pagination');
const LIST_PAGE_SIZE = 10;
function toIsoTimestamp(value) {
if (!value) {
@@ -195,8 +198,21 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
app.get('/data-sources/api-sources', requirePermission('api-sources.read'), async function (req, res, next) {
try {
const data = await common.fetchApiSourcesData(pool);
res.send(pages.renderApiSourcesPage(data, req.query.message ? String(req.query.message) : '', req.currentUser, formatDashboardDate));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await common.fetchApiSourcesPage(pool, page, LIST_PAGE_SIZE);
const apiSources = (data.apiSources || []).map(function (apiSource) {
return Object.assign({}, apiSource, {
intervalLabel: apiSource.update_interval_unit === 'seconds'
? (Math.max(1, Number(apiSource.update_interval_value) || 0) === 1 ? 'Every second' : `Every ${Math.max(1, Number(apiSource.update_interval_value) || 0)} seconds`)
: (Math.max(1, Number(apiSource.update_interval_value) || 0) === 1 ? 'Every minute' : `Every ${Math.max(1, Number(apiSource.update_interval_value) || 0)} minutes`),
lastPullLabel: apiSource.last_pulled_at ? formatDashboardDate(apiSource.last_pulled_at) : 'Never',
lastPulledAtValue: apiSource.last_pulled_at ? new Date(apiSource.last_pulled_at).toISOString() : ''
});
});
res.send(pages.renderApiSourcesPage({
apiSources: apiSources,
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'API sources', 'API source pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser, formatDashboardDate));
} catch (error) {
next(error);
}
@@ -364,8 +380,12 @@ module.exports = function registerAdminDataSourceRoutes(app, deps) {
app.get('/data-sources/rss-feeds', requirePermission('rss-feeds.read'), async function (req, res, next) {
try {
const data = await common.fetchRssFeedsData(pool);
res.send(pages.renderRssFeedsPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await common.fetchRssFeedsPage(pool, page, LIST_PAGE_SIZE);
res.send(pages.renderRssFeedsPage({
rssFeeds: data.rssFeeds || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'RSS feeds', 'RSS feed pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
+4 -2
View File
@@ -76,6 +76,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
try {
const name = String(req.body.name || '').trim();
const fadeBetweenSlides = req.body.fade_between_slides ? 1 : 0;
const skipUnavailableRtmp = req.body.skip_unavailable_rtmp ? 1 : 0;
if (!name) {
return res.status(400).send('Playlist name is required.');
}
@@ -83,7 +84,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
return res.status(400).send('A playlist with that name already exists.');
}
const actorId = getAuditUserId(req);
const [result] = await pool.query('INSERT INTO playlists (name, fade_between_slides, created_by, modified_by) VALUES (?, ?, ?, ?)', [name, fadeBetweenSlides, actorId, actorId]);
const [result] = await pool.query('INSERT INTO playlists (name, fade_between_slides, skip_unavailable_rtmp, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [name, fadeBetweenSlides, skipUnavailableRtmp, actorId, actorId]);
redirectAfterSave(req, res, '/playlists?edit=' + result.insertId, {
closeUrl: '/playlists',
newUrl: '/playlists/new',
@@ -99,6 +100,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
try {
const name = String(req.body.name || '').trim();
const fadeBetweenSlides = req.body.fade_between_slides ? 1 : 0;
const skipUnavailableRtmp = req.body.skip_unavailable_rtmp ? 1 : 0;
if (!name) {
return res.status(400).send('Playlist name is required.');
}
@@ -220,7 +222,7 @@ module.exports = function registerAdminManageRoutes(app, deps) {
const actorId = getAuditUserId(req);
await connection.beginTransaction();
await connection.query('UPDATE playlists SET name = ?, fade_between_slides = ?, modified_by = ? WHERE id = ?', [name, fadeBetweenSlides, actorId, playlist.id]);
await connection.query('UPDATE playlists SET name = ?, fade_between_slides = ?, skip_unavailable_rtmp = ?, modified_by = ? WHERE id = ?', [name, fadeBetweenSlides, skipUnavailableRtmp, actorId, playlist.id]);
await connection.query('DELETE FROM playlist_slides WHERE playlist_id = ?', [playlist.id]);
for (let i = 0; i < normalizedSlides.length; i += 1) {
const item = normalizedSlides[i];
+9 -2
View File
@@ -7,8 +7,11 @@ module.exports = function registerAdminRbacRoutes(app, deps) {
const permissions = Array.isArray(deps.permissions) ? deps.permissions : [];
const readArrayField = deps.readArrayField;
const normalizePermissionKeys = deps.normalizePermissionKeys;
const { buildPagination } = require('../../lib/pagination');
const requirePermission = deps.requirePermission;
const LIST_PAGE_SIZE = 10;
function slugifyRoleKey(name) {
const value = String(name || '').trim().toLowerCase();
const slug = value.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
@@ -158,8 +161,12 @@ module.exports = function registerAdminRbacRoutes(app, deps) {
app.get('/rbac', requirePermission('rbac.read'), async function (req, res, next) {
try {
const roles = await rbacData.fetchRoles(pool);
res.send(pages.renderRbacPage({ roles: roles }, req.query.message ? String(req.query.message) : '', req.currentUser));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await rbacData.fetchRolesPage(pool, page, LIST_PAGE_SIZE);
res.send(pages.renderRbacPage({
roles: data.roles || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'roles', 'Role pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
+10 -3
View File
@@ -7,8 +7,11 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
const hashPassword = deps.hashPassword;
const readArrayField = deps.readArrayField;
const rbacData = deps.rbacData;
const { buildPagination } = require('../../lib/pagination');
const requirePermission = deps.requirePermission;
const LIST_PAGE_SIZE = 10;
async function fetchRoleOptions() {
return rbacData.fetchRoles(pool);
}
@@ -53,8 +56,9 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
app.get('/users', requirePermission('users.read'), async function (req, res, next) {
try {
const users = await rbacData.fetchUsersWithRoles(pool);
const mappedUsers = users.map(function (user) {
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await rbacData.fetchUsersWithRolesPage(pool, page, LIST_PAGE_SIZE);
const mappedUsers = (data.users || []).map(function (user) {
return Object.assign({}, user, {
isCurrentUser: Number(user.id) === Number(req.currentUser.id),
createdAtLabel: formatDashboardDate(user.created_at),
@@ -62,7 +66,10 @@ module.exports = function registerAdminUsersRoutes(app, deps) {
roleNames: String(user.roleNames || '').trim() || 'No roles assigned'
});
});
res.send(pages.renderUsersPage({ users: mappedUsers }, req.query.message ? String(req.query.message) : '', req.currentUser));
res.send(pages.renderUsersPage({
users: mappedUsers,
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'users', 'Users pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
@@ -43,6 +43,7 @@ module.exports = function renderApiSourcesPage(data, message, currentUser, forma
active: 'api-sources',
message: message,
currentUser: currentUser || null,
apiSources: apiSources
apiSources: apiSources,
pagination: data.pagination || null
});
};
@@ -27,6 +27,7 @@ module.exports = function renderRssFeedsPage(data, message, currentUser) {
active: 'rss-feeds',
message: message,
currentUser: currentUser || null,
rssFeeds: rssFeeds
rssFeeds: rssFeeds,
pagination: data.pagination || null
});
};
@@ -2,6 +2,7 @@ const { renderView } = require('../../view');
const { hasAnyPermission } = require('../../../rbac');
const TASKS_PER_PAGE = 10;
const TASK_STATUSES = new Set(['queued', 'running', 'failed', 'completed', 'canceled']);
function formatIntervalLabel(intervalMs) {
const value = Math.max(1, Number(intervalMs) || 0);
@@ -21,7 +22,40 @@ function parsePageNumber(value) {
return Math.max(1, pageNumber);
}
function buildPagination(totalItems, currentPage, pageParam) {
function normalizeStatusFilter(value) {
const status = String(value || '').trim().toLowerCase();
return TASK_STATUSES.has(status) ? status : '';
}
function normalizeSourceFilter(sourceType, sourceId) {
const normalizedSourceType = String(sourceType || '').trim();
const normalizedSourceId = Math.floor(Number(sourceId) || 0);
if (!normalizedSourceType || !Number.isFinite(normalizedSourceId) || normalizedSourceId <= 0) {
return { sourceType: '', sourceId: '' };
}
return {
sourceType: normalizedSourceType,
sourceId: normalizedSourceId
};
}
function buildQueryString(query) {
const searchParams = new URLSearchParams();
['page', 'recurringPage', 'status', 'sourceType', 'sourceId'].forEach(function (key) {
const value = query && query[key];
if (value === undefined || value === null || value === '') {
return;
}
searchParams.set(key, String(value));
});
const queryString = searchParams.toString();
return queryString ? `?${queryString}` : '';
}
function buildPagination(totalItems, currentPage, pageParam, queryState) {
const normalizedPageParam = String(pageParam || 'page').trim() || 'page';
const totalPages = Math.max(1, Math.ceil(totalItems / TASKS_PER_PAGE));
const safeCurrentPage = Math.min(Math.max(1, currentPage), totalPages);
@@ -30,13 +64,17 @@ function buildPagination(totalItems, currentPage, pageParam) {
const pages = [];
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
const nextQuery = Object.assign({}, queryState, { [normalizedPageParam]: pageNumber });
pages.push({
number: pageNumber,
active: pageNumber === safeCurrentPage,
url: `?${normalizedPageParam}=${pageNumber}`
url: buildQueryString(nextQuery)
});
}
const previousQuery = Object.assign({}, queryState, { [normalizedPageParam]: safeCurrentPage - 1 });
const nextQuery = Object.assign({}, queryState, { [normalizedPageParam]: safeCurrentPage + 1 });
return {
currentPage: safeCurrentPage,
totalPages: totalPages,
@@ -46,23 +84,139 @@ function buildPagination(totalItems, currentPage, pageParam) {
endItem: endIndex,
hasPrevious: safeCurrentPage > 1,
hasNext: safeCurrentPage < totalPages,
previousUrl: `?${normalizedPageParam}=${safeCurrentPage - 1}`,
nextUrl: `?${normalizedPageParam}=${safeCurrentPage + 1}`,
previousUrl: buildQueryString(previousQuery),
nextUrl: buildQueryString(nextQuery),
pages: pages,
pageSize: TASKS_PER_PAGE,
pageParam: normalizedPageParam
};
}
function taskMatchesFilters(task, statusFilter, sourceFilter) {
if (statusFilter && task.status !== statusFilter) {
return false;
}
if (!sourceFilter || !sourceFilter.sourceType || !sourceFilter.sourceId) {
return true;
}
const metadata = task.metadata || {};
return String(metadata.sourceType || '').trim() === sourceFilter.sourceType && Number(metadata.sourceId || 0) === sourceFilter.sourceId;
}
function formatStatusLabel(status) {
const normalizedStatus = String(status || '').trim().toLowerCase();
if (!normalizedStatus) {
return 'All tasks';
}
return normalizedStatus.charAt(0).toUpperCase() + normalizedStatus.slice(1);
}
function buildTaskSourceFilterUrl(queryState, task, sourceFilter) {
const metadata = task.metadata || {};
const sourceType = String(metadata.sourceType || '').trim();
const sourceId = Math.floor(Number(metadata.sourceId) || 0);
if (!sourceType || !Number.isFinite(sourceId) || sourceId <= 0) {
return '';
}
const isActive = sourceFilter && sourceFilter.sourceType === sourceType && sourceFilter.sourceId === sourceId;
const nextQuery = Object.assign({}, queryState, {
page: 1,
sourceType: isActive ? '' : sourceType,
sourceId: isActive ? '' : sourceId
});
return buildQueryString(nextQuery);
}
function buildStatusFilterUrl(queryState, statusFilter) {
const nextQuery = Object.assign({}, queryState, {
page: 1,
status: statusFilter && statusFilter.status === statusFilter.targetStatus ? '' : statusFilter.targetStatus,
sourceType: statusFilter && statusFilter.sourceType ? statusFilter.sourceType : '',
sourceId: statusFilter && statusFilter.sourceId ? statusFilter.sourceId : ''
});
return buildQueryString(nextQuery);
}
module.exports = function renderBackgroundTasksPage(data, message, currentUser) {
const tasks = (data && data.tasks) || [];
const recurringTasks = (data && data.recurringTasks) || [];
const summary = (data && data.summary) || { counts: {}, total: 0, activeCount: 0 };
const canManage = hasAnyPermission(currentUser, ['background-tasks.allow']);
const pagination = buildPagination(tasks.length, parsePageNumber(data && data.page), 'page');
const recurringPagination = buildPagination(recurringTasks.length, parsePageNumber(data && data.recurringPage), 'recurringPage');
const visibleTasks = tasks.slice((pagination.currentPage - 1) * TASKS_PER_PAGE, pagination.currentPage * TASKS_PER_PAGE);
const queryState = {
page: parsePageNumber(data && data.page),
recurringPage: parsePageNumber(data && data.recurringPage),
status: normalizeStatusFilter(data && data.status),
sourceType: '',
sourceId: ''
};
const sourceFilter = normalizeSourceFilter(data && data.sourceType, data && data.sourceId);
queryState.status = normalizeStatusFilter(data && data.status);
queryState.sourceType = sourceFilter.sourceType;
queryState.sourceId = sourceFilter.sourceId;
const filteredTasks = tasks.filter(function (task) {
return taskMatchesFilters(task, queryState.status, sourceFilter);
});
const pagination = buildPagination(filteredTasks.length, queryState.page, 'page', queryState);
const recurringPagination = buildPagination(recurringTasks.length, queryState.recurringPage, 'recurringPage', queryState);
const visibleTasks = filteredTasks.slice((pagination.currentPage - 1) * TASKS_PER_PAGE, pagination.currentPage * TASKS_PER_PAGE);
const visibleRecurringTasks = recurringTasks.slice((recurringPagination.currentPage - 1) * TASKS_PER_PAGE, recurringPagination.currentPage * TASKS_PER_PAGE);
const statusCards = [
{
status: 'queued',
label: 'Queued',
count: summary.counts.queued,
variant: 'primary'
},
{
status: 'running',
label: 'Running',
count: summary.counts.running,
variant: 'info'
},
{
status: 'failed',
label: 'Failed',
count: summary.counts.failed,
variant: 'danger'
},
{
status: 'completed',
label: 'Finished',
count: summary.counts.completed,
variant: 'success'
}
].map(function (card) {
const isActive = queryState.status === card.status;
const statusFilter = {
status: queryState.status,
sourceType: queryState.sourceType,
sourceId: queryState.sourceId,
targetStatus: card.status
};
return Object.assign({}, card, {
active: isActive,
href: buildStatusFilterUrl(queryState, statusFilter),
ariaLabel: isActive ? `Clear ${card.label.toLowerCase()} filter` : `Filter tasks by ${card.label.toLowerCase()}`
});
});
const visibleTasksWithSources = visibleTasks.map(function (task) {
return Object.assign({}, task, {
sourceUrl: buildTaskSourceFilterUrl(queryState, task, sourceFilter),
sourceLabel: task.metadata && task.metadata.sourceName ? String(task.metadata.sourceName).trim() : '',
sourceTypeLabel: task.metadata && task.metadata.sourceType ? String(task.metadata.sourceType).trim() : '',
sourceIdLabel: task.metadata && task.metadata.sourceId ? Number(task.metadata.sourceId) : ''
});
});
const hasActiveFilters = Boolean(queryState.status || queryState.sourceType || queryState.sourceId);
return renderView('settings/background-tasks/index', {
title: 'Background tasks',
@@ -71,13 +225,18 @@ module.exports = function renderBackgroundTasksPage(data, message, currentUser)
currentUser: currentUser || null,
scripts: ['js/settings/background-tasks.js'],
stateVersion: data && data.stateVersion ? String(data.stateVersion) : '',
tasks: visibleTasks,
tasks: visibleTasksWithSources,
recurringTasks: visibleRecurringTasks.map(function (task) {
return Object.assign({}, task, {
intervalLabel: formatIntervalLabel(task.intervalMs)
});
}),
summary: summary,
statusCards: statusCards,
activeStatusLabel: formatStatusLabel(queryState.status),
activeSourceLabel: queryState.sourceType && queryState.sourceId ? `${queryState.sourceType} #${queryState.sourceId}` : '',
clearFiltersUrl: buildQueryString({ recurringPage: queryState.recurringPage }),
hasActiveFilters: hasActiveFilters,
pagination: pagination,
recurringPagination: recurringPagination,
canManage: canManage,
+2 -1
View File
@@ -6,6 +6,7 @@ module.exports = function renderRbacPage(data, message, currentUser) {
active: 'rbac',
message: message,
currentUser: currentUser || null,
roles: data.roles || []
roles: data.roles || [],
pagination: data.pagination || null
});
};
+2 -1
View File
@@ -6,6 +6,7 @@ module.exports = function renderUsersPage(data, message, currentUser) {
active: 'users',
message: message,
currentUser: currentUser || null,
users: data.users || []
users: data.users || [],
pagination: data.pagination || null
});
};
+5 -2
View File
@@ -6,7 +6,9 @@ module.exports = function renderCanvasSizesPage(data, message, currentUser) {
name: size.name,
width: size.width,
height: size.height,
templateCount: (data.templates || []).filter((template) => Number(template.canvas_size_id) === Number(size.id)).length
templateCount: size.template_count !== undefined
? Number(size.template_count) || 0
: (data.templates || []).filter((template) => Number(template.canvas_size_id) === Number(size.id)).length
}));
return renderView('canvas-sizes/list', {
@@ -14,6 +16,7 @@ module.exports = function renderCanvasSizesPage(data, message, currentUser) {
active: 'canvas-sizes',
message: message,
currentUser: currentUser || null,
canvasSizes: canvasSizes
canvasSizes: canvasSizes,
pagination: data.pagination || null
});
};
@@ -2,12 +2,19 @@ module.exports = function registerCanvasSizeRoutes(app, deps) {
const pages = deps.pages;
const common = deps.common;
const pool = deps.pool;
const { buildPagination } = require('../../../lib/pagination');
const requirePermission = deps.requirePermission;
const LIST_PAGE_SIZE = 10;
app.get('/canvas-sizes', requirePermission('canvas-sizes.read'), async function (req, res, next) {
try {
const data = await common.fetchCanvasSizesData(pool);
res.send(pages.renderCanvasSizesPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await common.fetchCanvasSizesPage(pool, page, LIST_PAGE_SIZE);
res.send(pages.renderCanvasSizesPage({
canvasSizes: data.canvasSizes || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'canvas sizes', 'Canvas size pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
+2 -1
View File
@@ -7,6 +7,7 @@ module.exports = function renderConnectedClientsPage(data, message, currentUser)
message: message,
currentUser: currentUser || null,
scripts: ['js/dashboard/dashboard-page.js'],
clients: data.clients || []
clients: data.clients || [],
pagination: data.pagination || null
});
};
+11 -1
View File
@@ -2,12 +2,22 @@ module.exports = function registerClientsRoutes(app, deps) {
const pool = deps.pool;
const pages = deps.pages;
const buildDashboardState = deps.buildDashboardState;
const { buildPagination } = require('../../../lib/pagination');
const requirePermission = deps.requirePermission;
const LIST_PAGE_SIZE = 10;
app.get('/clients', requirePermission('clients.read'), async function (req, res, next) {
try {
const data = await buildDashboardState(pool);
res.send(pages.renderConnectedClientsPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const totalItems = Array.isArray(data.clients) ? data.clients.length : 0;
const startIndex = (page - 1) * LIST_PAGE_SIZE;
const clients = (data.clients || []).slice(startIndex, startIndex + LIST_PAGE_SIZE);
res.send(pages.renderConnectedClientsPage({
clients: clients,
pagination: buildPagination(totalItems, page, 'page', {}, LIST_PAGE_SIZE, 'clients', 'Client pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
+2 -1
View File
@@ -5,7 +5,8 @@ module.exports = function renderPlaylistFormPage(message, currentUser) {
title: 'Create playlist',
active: 'playlists',
playlist: {
fade_between_slides: false
fade_between_slides: false,
skip_unavailable_rtmp: false
},
message: message,
currentUser: currentUser || null
+2 -1
View File
@@ -102,6 +102,7 @@ module.exports = function renderPlaylistEditPage(playlist, data, message, curren
currentUser: currentUser || null,
playlist: playlist,
playlistFadeBetweenSlides: Boolean(playlist.fade_between_slides),
playlistSkipUnavailableRtmp: Boolean(playlist.skip_unavailable_rtmp),
playlistSlides: playlistSlides,
slides: data.slides || [],
hasSlides: (data.slides || []).length > 0,
@@ -110,6 +111,6 @@ module.exports = function renderPlaylistEditPage(playlist, data, message, curren
selectableSlides: selectableSlides,
playlistCanvasSignature: playlistCanvasSignature,
playlistCanvasMismatch: playlistCanvasMismatch,
scripts: ['js/vendor/sortable.min.js', 'js/playlists/playlist-schedule.js']
scripts: ['js/lib/modal.js', 'js/vendor/sortable.min.js', 'js/playlists/playlist-schedule.js']
});
};
+5 -3
View File
@@ -1,9 +1,10 @@
const { renderView } = require('../../../view');
module.exports = function renderPlaylistsPage(data, message, currentUser) {
const playlistSlides = data.playlistSlides || [];
const playlists = (data.playlists || []).map((playlist) => {
const slideCount = playlistSlides.filter((item) => item.playlist_id === playlist.id).length;
const slideCount = playlist.slide_count !== undefined
? Number(playlist.slide_count) || 0
: (data.playlistSlides || []).filter((item) => item.playlist_id === playlist.id).length;
return Object.assign({}, playlist, {
slideCount: slideCount,
slideCountIsOne: slideCount === 1
@@ -15,7 +16,8 @@ module.exports = function renderPlaylistsPage(data, message, currentUser) {
active: 'playlists',
message: message,
currentUser: currentUser || null,
playlists: playlists
playlists: playlists,
pagination: data.pagination || null
});
};
+10 -2
View File
@@ -2,8 +2,11 @@ module.exports = function registerPlaylistsRoutes(app, deps) {
const pool = deps.pool;
const common = deps.common;
const pages = deps.pages;
const { buildPagination } = require('../../../lib/pagination');
const requirePermission = deps.requirePermission;
const LIST_PAGE_SIZE = 10;
function requireQueryPermission(readPermissionKey, editPermissionKey) {
return function (req, res, next) {
const permissionKey = req.query && req.query.edit ? editPermissionKey : readPermissionKey;
@@ -13,15 +16,20 @@ module.exports = function registerPlaylistsRoutes(app, deps) {
app.get('/playlists', requireQueryPermission('playlists.read', 'playlists.update'), async function (req, res, next) {
try {
const data = await common.fetchAdminData(pool);
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
if (req.query.edit) {
const data = await common.fetchAdminData(pool);
const playlist = await common.fetchPlaylistById(pool, Number(req.query.edit));
if (!playlist) {
return res.status(404).send('Playlist not found');
}
return res.send(pages.renderPlaylistEditPage(playlist, data, req.query.message ? String(req.query.message) : '', req.currentUser));
}
res.send(pages.renderPlaylistsPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
const data = await common.fetchPlaylistsPage(pool, page, LIST_PAGE_SIZE);
res.send(pages.renderPlaylistsPage({
playlists: data.playlists || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'playlists', 'Playlist pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
@@ -74,6 +74,6 @@ module.exports = function renderPlaylistSlideConfigPage(playlist, playlistSlide,
...day,
checked: selectedDays.has(day.value)
})),
scripts: ['js/playlists/playlist-schedule.js']
scripts: ['js/lib/modal.js', 'js/playlists/playlist-schedule.js']
});
};
+2 -1
View File
@@ -6,7 +6,8 @@ module.exports = function renderScreensPage(data, message, currentUser) {
active: 'screens',
message: message,
currentUser: currentUser || null,
screens: data.screens || []
screens: data.screens || [],
pagination: data.pagination || null
});
};
+10 -2
View File
@@ -3,8 +3,11 @@ module.exports = function registerScreensRoutes(app, deps) {
const common = deps.common;
const pages = deps.pages;
const buildDashboardState = deps.buildDashboardState;
const { buildPagination } = require('../../../lib/pagination');
const requirePermission = deps.requirePermission;
const LIST_PAGE_SIZE = 10;
function requireQueryPermission(readPermissionKey, editPermissionKey) {
return function (req, res, next) {
const permissionKey = req.query && req.query.edit ? editPermissionKey : readPermissionKey;
@@ -14,8 +17,8 @@ module.exports = function registerScreensRoutes(app, deps) {
app.get('/screens', requireQueryPermission('screens.read', 'screens.update'), async function (req, res, next) {
try {
const data = await buildDashboardState(pool);
if (req.query.edit) {
const data = await buildDashboardState(pool);
const screen = await common.fetchScreenById(pool, Number(req.query.edit));
if (!screen) {
return res.status(404).send('Screen not found');
@@ -23,7 +26,12 @@ module.exports = function registerScreensRoutes(app, deps) {
const editData = await common.fetchScreenEditData(pool);
return res.send(pages.renderScreenEditPage(screen, editData, req.query.message ? String(req.query.message) : '', req.currentUser));
}
res.send(pages.renderScreensPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await common.fetchScreensPage(pool, page, LIST_PAGE_SIZE);
res.send(pages.renderScreensPage({
screens: data.screens || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'screens', 'Screen pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
+2 -1
View File
@@ -6,7 +6,8 @@ module.exports = function renderSlidesPage(data, message, currentUser) {
active: 'slides',
message: message,
currentUser: currentUser || null,
slides: data.slides || []
slides: data.slides || [],
pagination: data.pagination || null
});
};
+9 -2
View File
@@ -2,12 +2,19 @@ module.exports = function registerSlidesRoutes(app, deps) {
const pages = deps.pages;
const common = deps.common;
const pool = deps.pool;
const { buildPagination } = require('../../../lib/pagination');
const requirePermission = deps.requirePermission;
const LIST_PAGE_SIZE = 10;
app.get('/slides', requirePermission('slides.read'), async function (req, res, next) {
try {
const data = await common.fetchAdminData(pool);
res.send(pages.renderSlidesPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await common.fetchSlidesPage(pool, page, LIST_PAGE_SIZE);
res.send(pages.renderSlidesPage({
slides: data.slides || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'slides', 'Slide pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
+1 -1
View File
@@ -50,6 +50,6 @@ module.exports = function renderTemplateFormPage(template, mode, message, canvas
currentUser: currentUser || null,
template: current,
canvasSizes: canvasSizes || [],
scripts: ['js/templates/template-designer-utils.js', 'js/templates/template-designer.js']
scripts: ['js/lib/modal.js', 'js/templates/template-designer-utils.js', 'js/templates/template-designer.js']
});
};
+5 -2
View File
@@ -6,7 +6,9 @@ module.exports = function renderTemplatesPage(data, message, currentUser) {
name: template.name,
canvas_size_width: template.canvas_size_width,
canvas_size_height: template.canvas_size_height,
regionCount: (data.templateRegions || []).filter((region) => region.template_id === template.id).length
regionCount: template.region_count !== undefined
? Number(template.region_count) || 0
: (data.templateRegions || []).filter((region) => region.template_id === template.id).length
}));
return renderView('templates/list', {
@@ -14,6 +16,7 @@ module.exports = function renderTemplatesPage(data, message, currentUser) {
active: 'templates',
message: message,
currentUser: currentUser || null,
templates: templates
templates: templates,
pagination: data.pagination || null
});
};
+9 -2
View File
@@ -2,12 +2,19 @@ module.exports = function registerTemplatesRoutes(app, deps) {
const pages = deps.pages;
const common = deps.common;
const pool = deps.pool;
const { buildPagination } = require('../../../lib/pagination');
const requirePermission = deps.requirePermission;
const LIST_PAGE_SIZE = 10;
app.get('/templates', requirePermission('templates.read'), async function (req, res, next) {
try {
const data = await common.fetchTemplatesData(pool);
res.send(pages.renderTemplatesPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const data = await common.fetchTemplatesPage(pool, page, LIST_PAGE_SIZE);
res.send(pages.renderTemplatesPage({
templates: data.templates || [],
pagination: buildPagination(data.totalItems, data.currentPage, 'page', {}, LIST_PAGE_SIZE, 'templates', 'Template pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}