Release v2.6.19
This commit is contained in:
@@ -9,7 +9,8 @@ function buildDuplicateTimetableGroup(timetableGroup, duplicateName) {
|
||||
return Object.assign({}, timetableGroup, {
|
||||
id: null,
|
||||
name: duplicateName,
|
||||
shortDescription: timetableGroup.short_description || ''
|
||||
shortDescription: timetableGroup.short_description || '',
|
||||
timezone: timetableGroup.timezone || 'UTC'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,40 @@
|
||||
// Shared timetable group form view-model builder.
|
||||
|
||||
const { normalizeTimeZone } = require('../../../../data/timetables');
|
||||
|
||||
const COMMON_TIME_ZONES = [
|
||||
'UTC',
|
||||
'Europe/London',
|
||||
'Europe/Dublin',
|
||||
'Europe/Paris',
|
||||
'Europe/Berlin',
|
||||
'Europe/Madrid',
|
||||
'America/New_York',
|
||||
'America/Chicago',
|
||||
'America/Denver',
|
||||
'America/Los_Angeles',
|
||||
'America/Toronto',
|
||||
'America/Vancouver',
|
||||
'America/Sao_Paulo',
|
||||
'Asia/Dubai',
|
||||
'Asia/Kolkata',
|
||||
'Asia/Singapore',
|
||||
'Asia/Tokyo',
|
||||
'Asia/Seoul',
|
||||
'Australia/Sydney',
|
||||
'Pacific/Auckland'
|
||||
];
|
||||
|
||||
function buildDefaultTimetableGroup() {
|
||||
return {
|
||||
id: null,
|
||||
name: '',
|
||||
shortDescription: ''
|
||||
shortDescription: '',
|
||||
timezone: 'Europe/London'
|
||||
};
|
||||
}
|
||||
|
||||
function formatDateTimeLocalValue(value) {
|
||||
function formatDateTimeLocalValue(value, timeZone) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
@@ -18,22 +44,49 @@ function formatDateTimeLocalValue(value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const year = String(date.getFullYear()).padStart(4, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
const resolvedTimeZone = normalizeTimeZone(timeZone, 'Europe/London');
|
||||
const parts = new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: resolvedTimeZone,
|
||||
hour12: false,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).formatToParts(date).reduce(function (acc, part) {
|
||||
if (part && part.type && part.type !== 'literal') {
|
||||
acc[part.type] = part.value;
|
||||
}
|
||||
return acc;
|
||||
}, Object.create(null));
|
||||
|
||||
if (!parts.year || !parts.month || !parts.day || !parts.hour || !parts.minute) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return [
|
||||
String(parts.year).padStart(4, '0'),
|
||||
'-',
|
||||
String(parts.month).padStart(2, '0'),
|
||||
'-',
|
||||
String(parts.day).padStart(2, '0'),
|
||||
'T',
|
||||
String(parts.hour).padStart(2, '0'),
|
||||
':',
|
||||
String(parts.minute).padStart(2, '0')
|
||||
].join('');
|
||||
}
|
||||
|
||||
function buildTimetableGroupFormViewModel(timetableGroup, timetableEntries, message, currentUser, isEdit, options) {
|
||||
const viewTimetableGroup = Object.assign(buildDefaultTimetableGroup(), timetableGroup || {});
|
||||
viewTimetableGroup.timezone = normalizeTimeZone(viewTimetableGroup.timezone, 'Europe/London');
|
||||
const viewOptions = options || {};
|
||||
const viewTimetableEntries = Array.isArray(timetableEntries) && timetableEntries.length
|
||||
? timetableEntries.map(function (entry) {
|
||||
return Object.assign({}, entry, {
|
||||
startValue: formatDateTimeLocalValue(entry.start_datetime),
|
||||
endValue: formatDateTimeLocalValue(entry.end_datetime)
|
||||
startValue: formatDateTimeLocalValue(entry.start_datetime, viewTimetableGroup.timezone),
|
||||
endValue: formatDateTimeLocalValue(entry.end_datetime, viewTimetableGroup.timezone)
|
||||
});
|
||||
})
|
||||
: [{ id: null, title: '', short_description: '', startValue: '', endValue: '', sort_order: 0 }];
|
||||
@@ -46,6 +99,7 @@ function buildTimetableGroupFormViewModel(timetableGroup, timetableEntries, mess
|
||||
messageVariant: viewOptions.messageVariant || '',
|
||||
isEdit: Boolean(isEdit),
|
||||
timetableGroup: viewTimetableGroup,
|
||||
timeZoneOptions: COMMON_TIME_ZONES,
|
||||
timetableEntries: viewTimetableEntries,
|
||||
inUse: Boolean(viewTimetableGroup.inUse),
|
||||
showSaveSecondaryActions: true,
|
||||
|
||||
@@ -1,22 +1,45 @@
|
||||
// Timetable group list page renderer.
|
||||
|
||||
const { renderView } = require('../../../view');
|
||||
const { normalizeTimeZone } = require('../../../../data/timetables');
|
||||
|
||||
function formatNextStartLabel(value, formatDashboardDate) {
|
||||
function formatDateInTimeZone(value, timeZone) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const resolvedTimeZone = normalizeTimeZone(timeZone, 'Europe/London');
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: resolvedTimeZone,
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatNextStartLabel(value, timeZone) {
|
||||
if (!value) {
|
||||
return 'No entries';
|
||||
}
|
||||
|
||||
const label = typeof formatDashboardDate === 'function'
|
||||
? formatDashboardDate(value)
|
||||
: String(value);
|
||||
const label = formatDateInTimeZone(value, timeZone) || String(value);
|
||||
return label || 'No entries';
|
||||
}
|
||||
|
||||
module.exports = function renderTimetableGroupsPage(data, message, currentUser, formatDashboardDate) {
|
||||
module.exports = function renderTimetableGroupsPage(data, message, currentUser) {
|
||||
const timetableGroups = (data.timetableGroups || []).map(function (timetableGroup) {
|
||||
const timeZone = normalizeTimeZone(timetableGroup.timezone, 'Europe/London');
|
||||
return Object.assign({}, timetableGroup, {
|
||||
nextStartLabel: formatNextStartLabel(timetableGroup.next_start_datetime, formatDashboardDate),
|
||||
timeZone: timeZone,
|
||||
nextStartLabel: formatNextStartLabel(timetableGroup.next_start_datetime, timeZone),
|
||||
nextStartValue: timetableGroup.next_start_datetime ? new Date(timetableGroup.next_start_datetime).toISOString() : ''
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ async function getDataSourceUsageMaps(pool, common) {
|
||||
return timetableGroupIds;
|
||||
}
|
||||
|
||||
function readScheduleArrayFieldValue(body, keys) {
|
||||
function readTimetableArrayFieldValue(body, keys) {
|
||||
const searchKeys = Array.isArray(keys) ? keys : [keys];
|
||||
for (let index = 0; index < searchKeys.length; index += 1) {
|
||||
const key = searchKeys[index];
|
||||
@@ -57,17 +57,17 @@ function readScheduleArrayFieldValue(body, keys) {
|
||||
return [];
|
||||
}
|
||||
|
||||
function buildScheduleEntryRows(req, parseDateTimeLocal) {
|
||||
function buildTimetableEntryRows(req, parseDateTimeLocal) {
|
||||
const body = req.body || {};
|
||||
const ids = readScheduleArrayFieldValue(body, ['entry_id[]', 'entry_id']);
|
||||
const titles = readScheduleArrayFieldValue(body, ['entry_title[]', 'entry_title']);
|
||||
const descriptions = readScheduleArrayFieldValue(body, ['entry_short_description[]', 'entry_short_description']);
|
||||
const starts = readScheduleArrayFieldValue(body, ['entry_start_datetime[]', 'entry_start_datetime']);
|
||||
const ends = readScheduleArrayFieldValue(body, ['entry_end_datetime[]', 'entry_end_datetime']);
|
||||
const ids = readTimetableArrayFieldValue(body, ['entry_id[]', 'entry_id']);
|
||||
const titles = readTimetableArrayFieldValue(body, ['entry_title[]', 'entry_title']);
|
||||
const descriptions = readTimetableArrayFieldValue(body, ['entry_short_description[]', 'entry_short_description']);
|
||||
const starts = readTimetableArrayFieldValue(body, ['entry_start_datetime[]', 'entry_start_datetime']);
|
||||
const ends = readTimetableArrayFieldValue(body, ['entry_end_datetime[]', 'entry_end_datetime']);
|
||||
|
||||
const lengths = [ids.length, titles.length, descriptions.length, starts.length, ends.length].filter(Boolean);
|
||||
if (lengths.length && lengths.some(function (value) { return value !== lengths[0]; })) {
|
||||
const error = new Error('Schedule entry data is invalid.');
|
||||
const error = new Error('Timetable entry data is invalid.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
@@ -86,19 +86,19 @@ function buildScheduleEntryRows(req, parseDateTimeLocal) {
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
const error = new Error('Each schedule entry requires a title.');
|
||||
const error = new Error('Each timetable entry requires a title.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!startDatetime) {
|
||||
const error = new Error('Each schedule entry requires a start datetime.');
|
||||
const error = new Error('Each timetable entry requires a start datetime.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (endDatetime && endDatetime < startDatetime) {
|
||||
const error = new Error('Schedule entry end datetime must be after the start datetime.');
|
||||
const error = new Error('Timetable entry end datetime must be after the start datetime.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
@@ -191,7 +191,7 @@ module.exports = function registerTimetableRoutes(app, deps) {
|
||||
|
||||
let duplicateName = buildDuplicateTimetableGroupName(timetableGroup.name);
|
||||
let duplicateIndex = 2;
|
||||
while (await common.fetchDuplicateName(pool, 'i_schedule_groups', duplicateName)) {
|
||||
while (await common.fetchDuplicateName(pool, 'i_timetable_groups', duplicateName)) {
|
||||
duplicateName = buildDuplicateTimetableGroupName(timetableGroup.name) + ' (' + duplicateIndex + ')';
|
||||
duplicateIndex += 1;
|
||||
}
|
||||
@@ -207,16 +207,16 @@ module.exports = function registerTimetableRoutes(app, deps) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
const payload = common.buildTimetableGroupPayload(req, null);
|
||||
if (await common.fetchDuplicateName(pool, 'i_schedule_groups', payload.name)) {
|
||||
if (await common.fetchDuplicateName(pool, 'i_timetable_groups', payload.name)) {
|
||||
return res.redirect('/data-sources/timetables/new?message=' + encodeURIComponent('A timetable group with that name already exists.'));
|
||||
}
|
||||
|
||||
const entryRows = buildScheduleEntryRows(req, parseDateTimeLocal);
|
||||
const entryRows = buildTimetableEntryRows(req, parseDateTimeLocal);
|
||||
const actorId = getAuditUserId(req);
|
||||
await connection.beginTransaction();
|
||||
const [result] = await connection.query(
|
||||
'INSERT INTO i_schedule_groups (name, short_description, created_by, modified_by) VALUES (?, ?, ?, ?)',
|
||||
[payload.name, payload.shortDescription || null, actorId, actorId]
|
||||
'INSERT INTO i_timetable_groups (name, short_description, timezone, created_by, modified_by) VALUES (?, ?, ?, ?, ?)',
|
||||
[payload.name, payload.shortDescription || null, payload.timezone, actorId, actorId]
|
||||
);
|
||||
|
||||
if (entryRows.length) {
|
||||
@@ -233,7 +233,7 @@ module.exports = function registerTimetableRoutes(app, deps) {
|
||||
});
|
||||
|
||||
await connection.query(
|
||||
'INSERT INTO i_schedule_entries (schedule_group_id, title, short_description, start_datetime, end_datetime, created_by, modified_by) VALUES ?',
|
||||
'INSERT INTO i_timetable_entries (schedule_group_id, title, short_description, start_datetime, end_datetime, created_by, modified_by) VALUES ?',
|
||||
[insertRows]
|
||||
);
|
||||
}
|
||||
@@ -265,18 +265,18 @@ module.exports = function registerTimetableRoutes(app, deps) {
|
||||
}
|
||||
|
||||
const payload = common.buildTimetableGroupPayload(req, timetableGroup);
|
||||
if (await common.fetchDuplicateName(pool, 'i_schedule_groups', payload.name, timetableGroup.id)) {
|
||||
if (await common.fetchDuplicateName(pool, 'i_timetable_groups', payload.name, timetableGroup.id)) {
|
||||
return res.redirect('/data-sources/timetables/' + timetableGroup.id + '/edit?message=' + encodeURIComponent('A timetable group with that name already exists.'));
|
||||
}
|
||||
|
||||
const entryRows = buildScheduleEntryRows(req, parseDateTimeLocal);
|
||||
const entryRows = buildTimetableEntryRows(req, parseDateTimeLocal);
|
||||
const actorId = getAuditUserId(req);
|
||||
await connection.beginTransaction();
|
||||
await connection.query(
|
||||
'UPDATE i_schedule_groups SET name = ?, short_description = ?, modified_by = ? WHERE id = ?',
|
||||
[payload.name, payload.shortDescription || null, actorId, timetableGroup.id]
|
||||
'UPDATE i_timetable_groups SET name = ?, short_description = ?, timezone = ?, modified_by = ? WHERE id = ?',
|
||||
[payload.name, payload.shortDescription || null, payload.timezone, actorId, timetableGroup.id]
|
||||
);
|
||||
await connection.query('DELETE FROM i_schedule_entries WHERE schedule_group_id = ?', [timetableGroup.id]);
|
||||
await connection.query('DELETE FROM i_timetable_entries WHERE schedule_group_id = ?', [timetableGroup.id]);
|
||||
|
||||
if (entryRows.length) {
|
||||
const insertRows = entryRows.map(function (entry) {
|
||||
@@ -292,7 +292,7 @@ module.exports = function registerTimetableRoutes(app, deps) {
|
||||
});
|
||||
|
||||
await connection.query(
|
||||
'INSERT INTO i_schedule_entries (schedule_group_id, title, short_description, start_datetime, end_datetime, created_by, modified_by) VALUES ?',
|
||||
'INSERT INTO i_timetable_entries (schedule_group_id, title, short_description, start_datetime, end_datetime, created_by, modified_by) VALUES ?',
|
||||
[insertRows]
|
||||
);
|
||||
}
|
||||
@@ -330,7 +330,7 @@ module.exports = function registerTimetableRoutes(app, deps) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
await connection.query('DELETE FROM i_schedule_groups WHERE id = ?', [timetableGroup.id]);
|
||||
await connection.query('DELETE FROM i_timetable_groups WHERE id = ?', [timetableGroup.id]);
|
||||
await connection.commit();
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
|
||||
Reference in New Issue
Block a user