* refactor: add typescript dependencies
* chore: update chakra-ui
* refactor: typescript config
* refactor: convert <MenuBar> component to typescript
* refactor: convert <TooltipActionBtn> component to typescript
* refactor: improve UX in file upload
* refactor: upgrade dependencies
* refactor: prepare data provider
* refactor: prevent importing bad fields
* refactor: extract merge to provider
* refactor(upload): parser merges only given fields
* refactor(upload): add event fields to excel
* refactor(upload): improve styling on modal open
* style: improve styling in menu
* style: prevent global pollution
* feat(upload): add upload options
* fix: avoid potential bug in log queue
This commit is contained in:
Carlos Valente
2022-09-17 23:28:52 +02:00
committed by GitHub
parent 94b03312a6
commit 3128f5a195
25 changed files with 788 additions and 368 deletions
@@ -0,0 +1,36 @@
/**
* Class Event Provider adds functions specific for handling event data
*/
export class DataProvider {
/**
* Merges two data objects
* @param {object} existing
* @param {object} newData
*/
static safeMerge(existing, newData) {
const mergedData = { ...existing };
if (typeof newData?.events !== 'undefined') {
mergedData.events = newData.events;
}
if (typeof newData?.event !== 'undefined') {
mergedData.event = { ...newData.event };
}
if (typeof newData?.settings !== 'undefined') {
mergedData.settings = { ...newData.settings };
}
if (typeof newData?.osc !== 'undefined') {
mergedData.osc = { ...newData.osc };
}
if (typeof newData?.http !== 'undefined') {
mergedData.http = { ...newData.http };
}
if (typeof newData?.aliases !== 'undefined') {
mergedData.aliases = [...newData.aliases];
}
if (typeof newData?.userFields !== 'undefined') {
mergedData.userFields = { ...existing.userFields, ...newData.userFields };
}
return mergedData;
}
}
@@ -0,0 +1,97 @@
import { DataProvider } from '../DataProvider';
describe('DataProvider', () => {
describe('safeMerge()', () => {
it('merges two objects ', () => {
const oldData = {
events: [{ event: 'old event' }],
event: {
title: 'old title',
url: 'old url',
endMessage: 'old end message',
},
osc: {
port: 'old port',
},
settings: {
app: 'ontime',
version: 1,
serverPort: 4001,
lock: null,
pinCode: null,
timeFormat: '24',
},
userFields: {
user0: 'old 0',
user1: 'old 1',
user2: 'old 2',
user3: 'old 3',
user4: 'old 4',
user5: 'old 5',
user6: 'old 6',
user7: 'old 7',
user8: 'old 8',
user9: 'old 9',
},
};
const newData = {
events: [{ event: 'new event' }],
event: {
title: 'new title',
url: 'new url',
endMessage: 'old end message',
publicInfo: 'new public info',
},
settings: {
app: 'ontime',
version: 1,
serverPort: 4001,
lock: null,
pinCode: null,
timeFormat: '24',
},
userFields: {
user6: 'new 6',
user7: 'new 7',
user8: 'new 8',
user9: 'new 9',
},
};
const expected = {
events: [{ event: 'new event' }],
event: {
title: 'new title',
url: 'new url',
publicInfo: 'new public info',
endMessage: 'old end message',
},
osc: {
port: 'old port',
},
settings: {
app: 'ontime',
version: 1,
serverPort: 4001,
lock: null,
pinCode: null,
timeFormat: '24',
},
userFields: {
user0: 'old 0',
user1: 'old 1',
user2: 'old 2',
user3: 'old 3',
user4: 'old 4',
user5: 'old 5',
user6: 'new 6',
user7: 'new 7',
user8: 'new 8',
user9: 'new 9',
},
};
const merged = DataProvider.safeMerge(oldData, newData);
expect(merged).toStrictEqual(expected);
});
});
});
+16 -30
View File
@@ -4,6 +4,7 @@ import { networkInterfaces } from 'os';
import { fileHandler } from '../utils/parser.js';
import { generateId } from '../utils/generate_id.js';
import { resolveDbPath } from '../modules/loadDb.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
// Create controller for GET request to '/ontime/poll'
// Returns data for current state
@@ -33,11 +34,7 @@ export const dbDownload = async (req, res) => {
});
};
/**
* @description Controller for POST request to /ontime/db
* @returns none
*/
const upload = async (file, req, res) => {
const uploadAndParse = async (file, req, res, options) => {
if (!fs.existsSync(file)) {
res.status(500).send({ message: 'Upload failed' });
return;
@@ -50,29 +47,18 @@ const upload = async (file, req, res) => {
res.status(400).send({ message: result.message });
} else if (result.message === 'success') {
// explicitly write objects
if (typeof result.data !== 'undefined') {
if (typeof result.data?.events !== 'undefined') {
data.events = result.data.events;
global.timer.setupWithEventList(result.data?.events);
}
if (typeof result.data?.event !== 'undefined') {
data.event = result.data.event;
}
if (typeof result.data?.settings !== 'undefined') {
data.settings = result.data.settings;
}
if (typeof result.data?.osc !== 'undefined') {
data.osc = result.data.osc;
}
if (typeof result.data?.http !== 'undefined') {
data.http = result.data.http;
}
if (typeof result.data?.aliases !== 'undefined') {
data.aliases = result.data.aliases;
}
if (typeof result.data?.userFields !== 'undefined') {
data.userFields = result.data.userFields;
if (typeof result !== 'undefined') {
if (!options.onlyEvents) {
const mergedData = DataProvider.safeMerge(data, result.data);
data.event = mergedData.event;
data.settings = mergedData.settings;
data.osc = mergedData.osc;
data.http = mergedData.http;
data.aliases = mergedData.aliases;
data.userFields = mergedData.userFields;
}
data.events = result.data.events || [];
global.timer.setupWithEventList(result.data.events || []);
await db.write();
}
res.sendStatus(200);
@@ -297,9 +283,9 @@ export const dbUpload = async (req, res) => {
res.status(400).send({ message: 'File not found' });
return;
}
const options = req.query;
const file = req.file.path;
upload(file, req, res);
uploadAndParse(file, req, res, options);
};
// Create controller for POST request to '/ontime/dbpath'
@@ -309,5 +295,5 @@ export const dbPathToUpload = async (req, res) => {
res.status(400).send({ message: 'Path to file not found' });
return;
}
upload(req.body.path, req, res);
uploadAndParse(req.body.path, req, res);
};
@@ -564,6 +564,9 @@ describe('test parseExcel function', () => {
[],
['Event Name', 'Test Event'],
['Event URL', 'www.carlosvalente.com'],
['Public Info', 'test public info'],
['Backstage Info', 'test backstage info'],
['End Message', 'test end message'],
[],
[],
[
@@ -640,6 +643,14 @@ describe('test parseExcel function', () => {
[],
];
const expectedParsedEvent = {
title: 'Test Event',
url: 'www.carlosvalente.com',
publicInfo: 'test public info',
backstageInfo: 'test backstage info',
endMessage: 'test end message',
};
const expectedParsedEvents = [
{
timeStart: 25200000,
@@ -681,6 +692,7 @@ describe('test parseExcel function', () => {
const parsedData = await parseExcel_v1(testdata);
expect(parsedData.event).toStrictEqual(expectedParsedEvent);
expect(parsedData.events).toBeDefined();
expect(parsedData.events.title).toBe(expectedParsedEvents.title);
expect(parsedData.events.presenter).toBe(expectedParsedEvents.presenter);
+25 -2
View File
@@ -68,6 +68,9 @@ export const parseExcel_v1 = async (excelData) => {
.forEach((row) => {
let eventTitleNext = false;
let eventUrlNext = false;
let publicInfoNext = false;
let backstageInfoNext = false;
let endMessageNext = false;
const event = {};
row.forEach((column, j) => {
@@ -78,6 +81,15 @@ export const parseExcel_v1 = async (excelData) => {
} else if (eventUrlNext) {
eventData.url = column;
eventUrlNext = false;
} else if (publicInfoNext) {
eventData.publicInfo = column;
publicInfoNext = false;
} else if (backstageInfoNext) {
eventData.backstageInfo = column;
backstageInfoNext = false;
} else if (endMessageNext) {
eventData.endMessage = column;
endMessageNext = false;
} else if (j === timeStartIndex) {
event.timeStart = parseExcelDate(column);
} else if (j === timeEndIndex) {
@@ -128,6 +140,15 @@ export const parseExcel_v1 = async (excelData) => {
case 'event url':
eventUrlNext = true;
break;
case 'public info':
publicInfoNext = true;
break;
case 'backstage info':
backstageInfoNext = true;
break;
case 'end message':
endMessageNext = true;
break;
case 'time start':
case 'start':
timeStartIndex = j;
@@ -329,7 +350,6 @@ export const fileHandler = async (file) => {
let res = {};
// check which file type are we dealing with
if (file.endsWith('.xlsx')) {
try {
const excelData = xlsx
@@ -341,7 +361,10 @@ export const fileHandler = async (file) => {
// we only look at worksheets called ontime or event schedule
if (excelData?.data) {
const dataFromExcel = await parseExcel_v1(excelData.data);
res.data = await parseJson_v1(dataFromExcel);
res.data = {};
res.data.events = parseEvents_v1(dataFromExcel);
res.data.event = parseEvent_v1(dataFromExcel, true);
res.data.userFields = parseUserFields_v1(dataFromExcel);
res.message = 'success';
} else {
console.log('Error: No sheets found named ontime or event schedule');
+67 -55
View File
@@ -14,37 +14,41 @@ export const parseEvents_v1 = (data) => {
if ('events' in data) {
console.log('Found events definition, importing...');
const events = [];
const ids = [];
for (const e of data.events) {
// cap number of events
if (events.length >= MAX_EVENTS) {
console.log(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
break;
}
// double check unique ids
if (ids.indexOf(e?.id) !== -1) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
if (e.type === 'event') {
const event = validateEvent_v1(e);
if (event != null) {
events.push(event);
ids.push(event.id);
try {
const ids = [];
for (const e of data.events) {
// cap number of events
if (events.length >= MAX_EVENTS) {
console.log(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
break;
}
// double check unique ids
if (ids.indexOf(e?.id) !== -1) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
if (e.type === 'event') {
const event = validateEvent_v1(e);
if (event != null) {
events.push(event);
ids.push(event.id);
}
} else if (e.type === 'delay') {
events.push({
...delayDef,
duration: e.duration,
id: e.id || generateId(),
});
} else if (e.type === 'block') {
events.push({ ...blockDef, id: e.id || generateId() });
} else {
console.log('ERROR: undefined event type, skipping');
}
} else if (e.type === 'delay') {
events.push({
...delayDef,
duration: e.duration,
id: e.id || generateId(),
});
} else if (e.type === 'block') {
events.push({ ...blockDef, id: e.id || generateId() });
} else {
console.log('ERROR: undefined event type, skipping');
}
} catch (error) {
console.log(`Error ${error}`);
}
// write to db
newEvents = events;
@@ -73,7 +77,7 @@ export const parseEvent_v1 = (data, enforce) => {
endMessage: e.endMessage || dbModelv1.event.endMessage,
};
} else if (enforce) {
newEvent = dbModelv1.event;
newEvent = { ...dbModelv1.event };
console.log(`Created event object in db`);
}
return newEvent;
@@ -137,7 +141,7 @@ export const parseOsc_v1 = (data, enforce) => {
...osc,
};
} else if (enforce) {
newOsc = dbModelv1.osc;
newOsc = { ...dbModelv1.osc };
console.log(`Created OSC object in db`);
}
return newOsc;
@@ -165,7 +169,7 @@ export const parseHttp_v1 = (data, enforce) => {
...http,
};
} else if (enforce) {
newHttp.http = dbModelv1.http;
newHttp.http = { ...dbModelv1.http };
console.log(`Created http object in db`);
}
return newHttp;
@@ -181,23 +185,27 @@ export const parseAliases_v1 = (data) => {
if ('aliases' in data) {
console.log('Found Aliases definition, importing...');
const ids = [];
for (const a of data.aliases) {
// double check unique ids
if (ids.indexOf(a?.id) !== -1) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
const newAlias = {
id: a.id || generateId(),
enabled: a.enabled || false,
alias: a.alias || '',
pathAndParams: a.pathAndParams || '',
};
try {
for (const a of data.aliases) {
// double check unique ids
if (ids.indexOf(a?.id) !== -1) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
const newAlias = {
id: a.id || generateId(),
enabled: a.enabled || false,
alias: a.alias || '',
pathAndParams: a.pathAndParams || '',
};
ids.push(newAlias.id);
newAliases.push(newAlias);
ids.push(newAlias.id);
newAliases.push(newAlias);
}
console.log(`Uploaded ${newAliases?.length || 0} alias(es)`);
} catch (error) {
console.log(`Error: ${error}`);
}
console.log(`Uploaded ${newAliases?.length || 0} alias(es)`);
}
return newAliases;
};
@@ -208,19 +216,23 @@ export const parseAliases_v1 = (data) => {
* @returns {object} - event object data
*/
export const parseUserFields_v1 = (data) => {
const newUserFields = dbModelv1.userFields;
const newUserFields = { ...dbModelv1.userFields };
if ('userFields' in data) {
console.log('Found User Fields definition, importing...');
// we will only be importing the fields we know, so look for that
let fieldsFound = 0;
for (const n in newUserFields) {
if (n in data.userFields) {
fieldsFound++;
newUserFields[n] = data.userFields[n];
try {
let fieldsFound = 0;
for (const n in newUserFields) {
if (n in data.userFields) {
fieldsFound++;
newUserFields[n] = data.userFields[n];
}
}
console.log(`Uploaded ${fieldsFound} user fields`);
} catch (error) {
console.log(`Error: ${error}`);
}
console.log(`Uploaded ${fieldsFound} user fields`);
}
return { ...dbModelv1.userFields, ...newUserFields };
return { ...newUserFields };
};