diff --git a/server/package.json b/server/package.json index 6a80c177c..8e281d8f2 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "ontime", - "version": "1.4.0", + "version": "1.4.1", "author": "Carlos Valente", "description": "Time keeping for live events", "repository": "https://github.com/cpvalente/ontime", diff --git a/server/src/classes/EventTimer.js b/server/src/classes/EventTimer.js index 7a78a1cd7..6057654dc 100644 --- a/server/src/classes/EventTimer.js +++ b/server/src/classes/EventTimer.js @@ -67,8 +67,7 @@ export class EventTimer extends Timer { // call general title reset this._resetSelection(); - this.numEvents = 0; - this._eventlist = null; + this._eventlist = []; this.onAir = false; // initialise socketIO server @@ -154,16 +153,17 @@ export class EventTimer extends Timer { * Broadcasts complete object state */ broadcastState() { + const numEvents = this._eventlist.length; this.broadcastTimer(); this.io.emit('playstate', this.state); this.io.emit('selected', { id: this.selectedEventId, index: this.selectedEventIndex, - total: this.numEvents, + total: numEvents, }); this.io.emit('selected-id', this.selectedEventId); this.io.emit('next-id', this.nextEventId); - this.io.emit('numevents', this.numEvents); + this.io.emit('numevents', numEvents); this.io.emit('publicselected-id', this.selectedPublicEventId); this.io.emit('publicnext-id', this.nextPublicEventId); this.io.emit('titles', this.titles); @@ -188,16 +188,17 @@ export class EventTimer extends Timer { */ trigger(action, payload) { let success = true; + const numEvents = this._eventlist.length; switch (action) { case 'start': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; // Call action and force update this.info('PLAYBACK', 'Play Mode Start'); this.start(); break; } case 'startById': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; const loaded = this.loadEventById(payload); if (loaded) { this.info('PLAYBACK', `Loaded event with ID ${payload}`); @@ -209,7 +210,7 @@ export class EventTimer extends Timer { break; } case 'startByIndex': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; const loaded = this.loadEventByIndex(payload); if (loaded) { this.info('PLAYBACK', `Loaded event with index ${payload}`); @@ -221,42 +222,42 @@ export class EventTimer extends Timer { break; } case 'pause': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; // Call action and force update this.info('PLAYBACK', 'Play Mode Pause'); this.pause(); break; } case 'stop': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; // Call action and force update this.info('PLAYBACK', 'Play Mode Stop'); this.stop(); break; } case 'roll': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; // Call action and force update this.info('PLAYBACK', 'Play Mode Roll'); this.roll(); break; } case 'previous': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; // Call action and force update this.info('PLAYBACK', 'Play Mode Previous'); this.previous(); break; } case 'next': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; // Call action and force update this.info('PLAYBACK', 'Play Mode Next'); this.next(); break; } case 'loadById': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; const loaded = this.loadEventById(payload); if (loaded) { this.info('PLAYBACK', `Loaded event with ID ${payload}`); @@ -266,7 +267,7 @@ export class EventTimer extends Timer { break; } case 'loadByIndex': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; const loaded = this.loadEventByIndex(payload); if (loaded) { this.info('PLAYBACK', `Loaded event with index ${payload}`); @@ -276,14 +277,14 @@ export class EventTimer extends Timer { break; } case 'unload': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; // Call action and force update this.info('PLAYBACK', 'Events unloaded'); this.unload(); break; } case 'reload': { - if (this.numEvents === 0 || this.numEvents == null) return false; + if (!numEvents) return false; // Call action and force update this.info('PLAYBACK', 'Reloaded event'); this.reload(); @@ -637,6 +638,20 @@ export class EventTimer extends Timer { socket.emit('playstate', this.state); }); + socket.on('set-loadid', (data) => { + this.trigger('loadById', data); + socket.emit('playstate', this.state); + }); + + socket.on('set-loadindex', (data) => { + const eventIndex = Number(data); + if (isNaN(eventIndex)) { + return; + } + this.trigger('loadByIndex', data); + socket.emit('playstate', this.state); + }); + socket.on('set-pause', () => { this.trigger('pause'); socket.emit('playstate', this.state); @@ -732,7 +747,7 @@ export class EventTimer extends Timer { socket.emit('selected', { id: this.selectedEventId, index: this.selectedEventIndex, - total: this.numEvents, + total: this._eventlist.length, }); }); @@ -741,7 +756,7 @@ export class EventTimer extends Timer { }); socket.on('get-numevents', () => { - socket.emit('numevents', this.numEvents); + socket.emit('numevents', this._eventlist.length); }); socket.on('get-next-id', () => { @@ -831,13 +846,12 @@ export class EventTimer extends Timer { // set general this._eventlist = []; - this.numEvents = 0; // update lifecycle: onStop this.ontimeCycle = this.cycleState.onStop; // update clients - this.broadcastThis('numevents', this.numEvents); + this.broadcastThis('numevents', this._eventlist.length); } /** @@ -845,7 +859,7 @@ export class EventTimer extends Timer { * @param {array} eventlist */ setupWithEventList(eventlist) { - if (!Array.isArray(eventlist) || eventlist.length < 1) return; + if (!Array.isArray(eventlist) || !eventlist.length) return; // filter only events const events = eventlist.filter((e) => e.type === 'event'); @@ -853,7 +867,6 @@ export class EventTimer extends Timer { // set general this._eventlist = events; - this.numEvents = numEvents; // list may contain no events if (numEvents < 1) return; @@ -873,16 +886,14 @@ export class EventTimer extends Timer { * @param {array} eventlist */ updateEventList(eventlist) { - // filter only events - const events = eventlist.filter((e) => e.type === 'event'); - const numEvents = events.length; + if (!Array.isArray(eventlist) || !eventlist.length) return; - // is this the first event - const first = this.numEvents === 0; + // filter only events + const events = eventlist.filter((e) => e.type === 'event' && !e.skip); + const numEvents = events.length; // set general this._eventlist = events; - this.numEvents = numEvents; // list may be empty if (numEvents < 1) { @@ -890,8 +901,8 @@ export class EventTimer extends Timer { return; } - // auto load if is the only event - if (first) { + // auto load if is the there was nothing before + if (!this._eventlist.length) { this.loadEvent(0); } else if (this.selectedEventId != null) { // handle reload selected @@ -925,7 +936,20 @@ export class EventTimer extends Timer { updateSingleEvent(id, entry) { // find object in events const eventIndex = this._eventlist.findIndex((e) => e.id === id); - if (eventIndex === -1) return; + if (eventIndex === -1) { + throw 'Event not found'; + } + + // check if event is set to be skipped + if (entry.skip) { + // stop event if running + if (id === this.selectedEventId) { + this.trigger('stop'); + } + + // delete event + this.deleteId(id); + } // update event in memory const e = this._eventlist[eventIndex]; @@ -961,6 +985,71 @@ export class EventTimer extends Timer { this.runCycle(); } + /** + * @description inserts an event after a given id + * @param event + * @param previousId + */ + insertEventAfterId(event, previousId) { + // find object in events + const previousIndex = this._eventlist.findIndex((e) => e.id === previousId); + if (previousIndex === -1) { + throw 'Event not found'; + } + + if (previousIndex + 1 >= this._eventlist.length) { + this._eventlist.push(event); + } else { + this._eventlist.splice(previousIndex + 1, 0, event); + } + + try { + // check if entry is running + if (event.id === this.selectedEventId) { + // handle reload selected + // Reload data if running + const type = + this.selectedEventId === event.id && this._startedAt != null ? 'reload' : 'load'; + this.loadEvent(this.selectedEventIndex, type); + } else if (event.id === this.nextEventId) { + // roll needs to recalculate + if (this.state === 'roll') { + this.rollLoad(); + } + } + + // load titles + if ('title' in event || 'subtitle' in event || 'presenter' in event) { + this._loadTitlesNext(); + this._loadTitlesNow(); + } + } catch (error) { + this.error('SERVER', error); + } + + // update clients + this.broadcastState(); + + // run cycle + this.runCycle(); + } + + /** + * @description inserts an event in the first position of the list + * @param event + */ + insertEventAtStart(event) { + // Insert at beginning + this._eventlist.unshift(event); + + // update clients + this.broadcastState(); + + // run cycle + this.runCycle(); + } + + /** * Deleted an event from the list by its id * @param {string} eventId @@ -972,7 +1061,6 @@ export class EventTimer extends Timer { // delete event and update count this._eventlist.splice(eventIndex, 1); - this.numEvents = this._eventlist.length; // reload data if necessary if (eventId === this.selectedEventId) { @@ -1017,7 +1105,7 @@ export class EventTimer extends Timer { * @param {number} eventIndex - Index of event in eventlist */ loadEventByIndex(eventIndex) { - if (eventIndex === -1 || eventIndex > this.numEvents) return false; + if (eventIndex === -1 || eventIndex > this._eventlist.length) return false; this.pause(); this.loadEvent(eventIndex, 'load'); // run cycle @@ -1194,11 +1282,13 @@ export class EventTimer extends Timer { this.titlesPublic.presenterNext = null; this.nextPublicEventId = null; - if (this.selectedEventIndex < this.numEvents - 1) { + const numEvents = this._eventlist.length; + + if (this.selectedEventIndex < numEvents - 1) { let nextPublic = false; let nextPrivate = false; - for (let i = this.selectedEventIndex + 1; i < this.numEvents; i++) { + for (let i = this.selectedEventIndex + 1; i < numEvents; i++) { // check that is the right type if (this._eventlist[i].type === 'event') { // if we have not set private @@ -1410,7 +1500,7 @@ export class EventTimer extends Timer { // do we need to change if (this.state === 'roll') return; - if (this.numEvents === 0 || this.numEvents == null) return; + if (!this._eventlist.length) return; // set state this.state = 'roll'; @@ -1424,7 +1514,7 @@ export class EventTimer extends Timer { previous() { // check that we have events to run - if (this.numEvents < 1) return; + if (!this._eventlist.length) return; // maybe this is the first event? if (this.selectedEventIndex === 0) return; @@ -1446,20 +1536,19 @@ export class EventTimer extends Timer { } next() { + const numEvents = this._eventlist.length; // check that we have events to run - if (this.numEvents < 1) return; + if (!numEvents) return; // maybe this is the last event? - if (this.selectedEventIndex === this.numEvents - 1) return; + if (this.selectedEventIndex === numEvents - 1) return; // if there is no event running, go to first if (this.selectedEventIndex === null) { this.loadEvent(0); } else { const gotoEvent = - this.selectedEventIndex < this.numEvents - 1 - ? this.selectedEventIndex + 1 - : this.numEvents - 1; + this.selectedEventIndex < numEvents - 1 ? this.selectedEventIndex + 1 : numEvents - 1; if (gotoEvent === this.selectedEventIndex) return; this.loadEvent(gotoEvent); } @@ -1489,7 +1578,7 @@ export class EventTimer extends Timer { * @description reloads current event */ reload() { - if (this.numEvents === 0 || this.numEvents == null) return; + if (!this._eventlist.length) return; // change playstate this.pause(); diff --git a/server/src/classes/__tests__/eventtimer.test.js b/server/src/classes/__tests__/eventtimer.test.js index 459159e1b..51845e602 100644 --- a/server/src/classes/__tests__/eventtimer.test.js +++ b/server/src/classes/__tests__/eventtimer.test.js @@ -68,8 +68,8 @@ test('object instantiates correctly', async () => { expect(t.nextEventId).toBeNull(); expect(t.selectedPublicEventId).toBeNull(); expect(t.nextPublicEventId).toBeNull(); - expect(t.numEvents).toBe(0); - expect(t._eventlist).toBeNull(); + expect(t._eventlist.length).toBe(0); + expect(t._eventlist).toStrictEqual([]); expect(t.onAir).toBeFalsy(); t.shutdown(); @@ -85,7 +85,7 @@ describe('test triggers behaviour', () => { }); test('does not allow triggering events with an empty list', (done) => { - expect(t.numEvents).toBe(0); + expect(t._eventlist.length).toBe(0); expect(t.trigger('start')).toBeFalsy(); expect(t.trigger('pause')).toBeFalsy(); @@ -104,7 +104,7 @@ describe('test triggers behaviour', () => { }); test('...and is consistent by calling the class methods', (done) => { - expect(t.numEvents).toBe(0); + expect(t._eventlist.length).toBe(0); expect(t.state).toBe('stop'); t.start(); diff --git a/server/src/controllers/eventsController.js b/server/src/controllers/eventsController.js index 632faa91d..4d2ab3898 100644 --- a/server/src/controllers/eventsController.js +++ b/server/src/controllers/eventsController.js @@ -9,6 +9,22 @@ import { } from '../models/eventsDefinition.js'; import { generateId } from '../utils/generate_id.js'; import { MAX_EVENTS } from '../settings.js'; +import { getPreviousPlayable } from '../utils/eventUtils.js'; + +async function _insertAndSync(newEvent) { + if (newEvent.order) { + const events = data.events; + await _insertAt(newEvent, newEvent.order); + const previousId = events?.[newEvent.order - 1]?.id; + _insertEventInTimerAfterId(newEvent, previousId); + } else if (newEvent.after) { + await _insertAfterId(newEvent, newEvent.after); + _insertEventInTimerAfterId(newEvent, newEvent.after); + } else { + await _insertAt(newEvent, 0); + _insertEventInTimerAfterId(newEvent); + } +} /** * Insets an event after a given index @@ -83,9 +99,32 @@ function _updateTimers() { global.timer.updateEventList(results); } -// Updates timer object single event -function _updateTimersSingle(id, entry) { - global.timer.updateSingleEvent(id, entry); +/** + * @description Adds an event to the timer after an event with given id + * @param {object} event + * @param {string} [previousId] + * @private + */ +function _insertEventInTimerAfterId(event, previousId) { + if (typeof previousId === 'undefined') { + global.timer.insertEventAtStart(event); + } else { + try { + global.timer.insertEventAfterId(event, previousId); + } catch (error) { + global.timer.error('SERVER', `Unable to update object: ${error}`); + } + } +} + +/** + * @description Updates timer object single event + * @param {string} id + * @param {object} event + * @private + */ +function _updateTimersSingle(id, event) { + global.timer.updateSingleEvent(id, event); } // Delete a single entry in timer object @@ -148,22 +187,9 @@ export const eventsPost = async (req, res) => { } try { - // get place where event should be - if (newEvent.order) { - await _insertAt(newEvent, newEvent.order); - } else if (newEvent.after) { - await _insertAfterId(newEvent, newEvent.after); - } else { - await _insertAt(newEvent, 0); - } - - // update timers - _updateTimers(); - - // reply OK + _insertAndSync(newEvent); res.sendStatus(201); } catch (error) { - console.log(error); res.status(400).send(error); } }; @@ -183,30 +209,41 @@ export const eventsPut = async (req, res) => { return; } - try { - const eventIndex = data.events.findIndex((e) => e.id === eventId); - if (eventIndex === -1) { - res.status(400).send(`No Id found found`); - return; - } + const eventIndex = data.events.findIndex((e) => e.id === eventId); + if (eventIndex === -1) { + res.status(400).send(`No Id found found`); + return; + } + try { const e = data.events[eventIndex]; data.events[eventIndex] = { ...e, ...req.body }; data.events[eventIndex].revision++; await db.write(); - // update timer - _updateTimersSingle(eventId, req.body); - + if (data.events[eventIndex].skip) { + _deleteTimerId(eventId); + // if it is a skip, i make sure it is deleted from timer + // event id might already not exist + } else { + try { + _updateTimersSingle(eventId, req.body); + } catch (error) { + if (error === 'Event not found') { + const { id: previousId } = getPreviousPlayable(data.events, e.id); + _insertEventInTimerAfterId(data.events[eventIndex], previousId); + } + } + } res.sendStatus(200); } catch (error) { - console.log(error); res.status(400).send(error); } }; // Create controller for PATCH request to '/events/' // Returns - +// DEPRECATED export const eventsPatch = async (req, res) => { // Code is the same as put, call that await eventsPut(req, res); @@ -247,7 +284,6 @@ export const eventsReorder = async (req, res) => { res.sendStatus(200); } catch (error) { - console.log(error); res.status(400).send(error); } }; diff --git a/server/src/models/eventsDefinition.js b/server/src/models/eventsDefinition.js index bc190f89e..60908c5a8 100644 --- a/server/src/models/eventsDefinition.js +++ b/server/src/models/eventsDefinition.js @@ -8,6 +8,7 @@ export const event = { timeType: 'start-end', duration: 0, isPublic: false, + skip: false, colour: '', user0: '', user1: '', diff --git a/server/src/routes/eventsRouter.js b/server/src/routes/eventsRouter.js index 9ea280c45..837a1e95b 100644 --- a/server/src/routes/eventsRouter.js +++ b/server/src/routes/eventsRouter.js @@ -27,6 +27,7 @@ router.post('/', eventsPost); router.put('/', eventsPut); // create route between controller and '/events/' endpoint +// DEPRECATED router.patch('/', eventsPatch); // create route between controller and '/events/reorder' endpoint diff --git a/server/src/utils/__tests__/eventUtils.test.js b/server/src/utils/__tests__/eventUtils.test.js new file mode 100644 index 000000000..6b91d3829 --- /dev/null +++ b/server/src/utils/__tests__/eventUtils.test.js @@ -0,0 +1,54 @@ +import { getPreviousPlayable } from '../eventUtils.js'; + +describe('getPreviousPlayable()', () => { + describe('given a list of events', () => { + it('finds the previous playable event', () => { + const events = [ + { id: 100, type: 'delay' }, + { id: 101, type: 'event', skip: true }, + { id: 102, type: 'event', skip: true }, + { id: 103, type: 'event', skip: false }, + { id: 'not-this', type: 'block' }, + { id: 104, type: 'event' }, + ]; + const { index, id } = getPreviousPlayable(events, events[4].id); + expect(index).toBe(3); + expect(id).toBe(103); + }); + }); + + describe('handles common errors', () => { + it('returns null if id not found in list', () => { + const events = [ + { id: 0, type: 'delay' }, + { id: 1, type: 'event', skip: true }, + { id: 2, type: 'event', skip: true }, + { id: 3, type: 'event', skip: false }, + { id: 4, type: 'event' }, + ]; + const { index, id } = getPreviousPlayable(events, 'no-valid-id'); + expect(index).toBe(null); + expect(id).toBe(null); + }); + + it('returns null if there are no previous events to play', () => { + const events = [ + { id: 0, type: 'delay' }, + { id: 1, type: 'event', skip: true }, + { id: 2, type: 'event', skip: true }, + { id: 3, type: 'event', skip: true }, + { id: 4, type: 'event' }, + ]; + const { index, id } = getPreviousPlayable(events, events[4].id); + expect(index).toBe(null); + expect(id).toBe(null); + }); + + it('returns null if list is empty', () => { + const events = []; + const { index, id } = getPreviousPlayable(events, 'made-up'); + expect(index).toBe(null); + expect(id).toBe(null); + }); + }); +}); \ No newline at end of file diff --git a/server/src/utils/__tests__/parser.tests.js b/server/src/utils/__tests__/parser.tests.js index 1222c35d6..dc2f7da21 100644 --- a/server/src/utils/__tests__/parser.tests.js +++ b/server/src/utils/__tests__/parser.tests.js @@ -1,6 +1,6 @@ import jest from 'jest-mock'; import { dbModelv1, dbModelv1 as dbModel } from '../../models/dataModel.js'; -import { parseExcel_v1, parseJson_v1, validateEvent_v1 } from '../parser.js'; +import { isStringEmpty, parseExcel_v1, parseJson_v1, validateEvent_v1 } from '../parser.js'; import { makeString, validateDuration } from '../parserUtils.js'; import { parseAliases_v1, parseUserFields_v1 } from '../parserUtils_v1.js'; @@ -42,7 +42,8 @@ describe('test json parser with valid def', () => { timeType: 'start-end', duration: 36000000 - 32400000, isPublic: true, - colour: '', + skip: true, + colour: 'red', type: 'event', revision: 0, id: 'f24d', @@ -92,6 +93,7 @@ describe('test json parser with valid def', () => { timeType: 'start-end', duration: 39000000 - 37200000, isPublic: true, + skip: false, colour: '', type: 'event', revision: 0, @@ -208,6 +210,7 @@ describe('test json parser with valid def', () => { timeType: 'start-end', duration: 32400000 - 31500000, isPublic: false, + skip: false, colour: '', type: 'event', revision: 0, @@ -226,6 +229,37 @@ describe('test json parser with valid def', () => { expect(first).toStrictEqual(expected); }); + it('second event is as a match', () => { + const second = parseResponse?.events[1]; + const expected = { + title: 'Good Morning', + subtitle: 'Days schedule', + presenter: 'Carlos Valente', + note: '', + timeStart: 32400000, + timeEnd: 36000000, + timeType: 'start-end', + duration: 36000000 - 32400000, + isPublic: true, + skip: true, + colour: 'red', + type: 'event', + revision: 0, + id: 'f24d', + user0: '', + user1: '', + user2: '', + user3: '', + user4: '', + user5: '', + user6: '', + user7: '', + user8: '', + user9: '', + }; + expect(second).toStrictEqual(expected); + }); + it('loaded event settings', () => { const eventTitle = parseResponse?.event?.title; expect(eventTitle).toBe('This is a test definition'); @@ -436,6 +470,7 @@ describe('test event validator', () => { timeStart: expect.any(Number), timeEnd: expect.any(Number), isPublic: expect.any(Boolean), + skip: expect.any(Boolean), revision: expect.any(Number), type: expect.any(String), id: expect.any(String), @@ -534,6 +569,7 @@ describe('test parseExcel function', () => { 'Presenter Name', 'Event Subtitle', 'Is Public? (x)', + 'Skip? (x)', 'Notes', 'User0:test0', 'User1:test1', @@ -554,6 +590,7 @@ describe('test parseExcel function', () => { 'Carlos', 'Getting things started', 'x', + '', 'Ballyhoo', '', '', @@ -578,6 +615,7 @@ describe('test parseExcel function', () => { 'Still Carlos', 'Derailing early', '', + '', 'Rainbow chase', '', '', @@ -606,6 +644,7 @@ describe('test parseExcel function', () => { presenter: 'Carlos', subtitle: 'Getting things started', isPublic: true, + skip: false, note: 'Ballyhoo', user0: 'a0', user1: 'a1', @@ -627,6 +666,7 @@ describe('test parseExcel function', () => { presenter: 'Still Carlos', subtitle: 'Derailing early', isPublic: false, + skip: true, note: 'Rainbow chase', user0: 'b0', user5: 'b5', @@ -642,6 +682,7 @@ describe('test parseExcel function', () => { expect(parsedData.events.presenter).toBe(expectedParsedEvents.presenter); expect(parsedData.events.subtitle).toBe(expectedParsedEvents.subtitle); expect(parsedData.events.isPublic).toBe(expectedParsedEvents.isPublic); + expect(parsedData.events.skip).toBe(expectedParsedEvents.skip); expect(parsedData.events.note).toBe(expectedParsedEvents.note); expect(parsedData.events.type).toBe(expectedParsedEvents.type); }); @@ -790,3 +831,24 @@ describe('test validateDuration()', () => { }); }); }); + +describe('isStringEmpty() function', () => { + describe('returns true with any non empty', () => { + const notEmpty = ['test', 'thisalso', '123', '#']; + for (const testValue of notEmpty) { + it(testValue, () => { + const isEmpty = isStringEmpty(testValue); + expect(isEmpty).toBe(false); + }); + } + }); + describe('returns true empty string or undefined', () => { + const empty = ['', ' ', undefined, null]; + for (const testValue of empty) { + it(`handles ${testValue}`, () => { + const isEmpty = isStringEmpty(testValue); + expect(isEmpty).toBe(true); + }); + } + }); +}); diff --git a/server/src/utils/eventUtils.js b/server/src/utils/eventUtils.js new file mode 100644 index 000000000..ae6681bd8 --- /dev/null +++ b/server/src/utils/eventUtils.js @@ -0,0 +1,25 @@ +/** + * @description Returns id of previous played event + * @param {array} events + * @param {string} eventId + * @return {object} + */ +export function getPreviousPlayable(events, eventId) { + // find current index + const current = events.findIndex((event) => event.id === eventId); + + if (current === -1) { + return { index: null, id: null }; + } + + let index = current - 1; + while (index >= 0) { + const event = events[index]; + if (event.type === 'event' && !event.skip) { + return { index, id: event.id }; + } + index--; + } + + return { index: null, id: null }; +} diff --git a/server/src/utils/parser.js b/server/src/utils/parser.js index d1988bb94..4d1954742 100644 --- a/server/src/utils/parser.js +++ b/server/src/utils/parser.js @@ -18,6 +18,19 @@ import { generateId } from './generate_id.js'; export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; export const JSON_MIME = 'application/json'; +/** + * @description Whether a string is considered empty + * @param value + * @return {boolean} + */ +export const isStringEmpty = (value) => { + let v = value; + if (typeof value === 'string') { + v = value.replace(/\s+/g, ''); + } + return v === '' || !v; +}; + /** * @description Excel array parser * @param {array} excelData - array with excel sheet @@ -36,6 +49,7 @@ export const parseExcel_v1 = async (excelData) => { let presenterIndex = null; let subtitleIndex = null; let isPublicIndex = null; + let skipIndex = null; let notesIndex = null; let colourIndex = null; let user0Index = null; @@ -75,12 +89,9 @@ export const parseExcel_v1 = async (excelData) => { } else if (j === subtitleIndex) { event.subtitle = column; } else if (j === isPublicIndex) { - // whether column is not empty - let c = column; - if (typeof column === 'string') { - c = column.replace(/\s+/g, ''); - } - event.isPublic = c !== ''; + event.isPublic = isStringEmpty(column); + } else if (j === skipIndex) { + event.skip = isStringEmpty(column); } else if (j === notesIndex) { event.note = column; } else if (j === colourIndex) { @@ -144,6 +155,11 @@ export const parseExcel_v1 = async (excelData) => { case 'public': isPublicIndex = j; break; + case 'skip? (x)': + case 'skip?': + case 'skip': + skipIndex = j; + break; case 'notes': notesIndex = j; break; @@ -278,7 +294,8 @@ export const validateEvent_v1 = (eventArgs) => { timeEnd: end, timeType: 'start-end', duration: validateDuration(start, end), - isPublic: e.isPublic != null && typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic, + isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic, + skip: typeof e.skip === 'boolean' ? e.skip : d.skip, note: makeString(e.note, d.note), user0: makeString(e.user0, d.user0), user1: makeString(e.user1, d.user1),