mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 12:53:32 +00:00
v2 (#386)
* Refactor/ts sockets (#210) * refactor(ts): create socket subscription service * refactor(ts-sockets): useSubscription hook * refactor(ts-sockets): extract colour selection to utility * Refactor/socket controller (#212) * refactor(socket): dictionary cleanup * refactor(socket): detangle socket from EventTimer * refactor(socket): poll object * refactor(socket): wip, extract data responsibilities to provider * fix: issue with onAir * refactor: create data provider and validation utils * refactor: remove deprecated endpoint * refactor: detangle and validate ontime controller * refactor: detangle and validate events controller * refactor: validate routers * refactor: handle post failure in modals * chore: upgrade electron (#215) * Fix: issue with text colour (#214) * fix: adjust text colour from context * fix: issue with wrong proptype * chore: upgrade electron * chore: declare node version * chore: upgrade node actions * Feat/55 v2 (#225) * chore: upgrade relevant libraries * feat(skip): add skip styling to paginated items * chore: upgrade relevant libraries * Fix: issue with text colour (#214) * fix: adjust text colour from context * fix: issue with wrong proptype * fix issue with vite migration (#217) * hotfix: 1.8.2 issues vite migration * fix: file import options * feat(55): styling * refactor: remove onhover option for entry block * refactor: relocate logging provider * refactor: convert to typescript * feat(55): add event editor * refactor: extract event actions * fix: bad import * feat(55): redesign components * fix: issues with not awaiting async * refactor: replace socket with subscription * refactor: remove unused * style: small tweaks * refactor: extract event actions * refactor: cleanup debug * refactor: chakra imports * fix: optimistic mutations * refactor: handle promise rejections * refactor: validation * fix: rq optimistic mutations * feat: add playback feedback to block * refactor: no optimistic adding of events * refactor: simplify cursor state * styles: cleanup editor style * refactor: cleanup duration update * fix: revert package upgrade (issues with vitest) * fix: prevent cyclic imports * refactor: extract data fetcher * refactor: typescript migration * chore: update tests * fix: remove problematic (#228) * styles: v2 (#229) * style: revise selected style * style: revise editor styles * style: revise viewer styles * chore: update tests * v2:feat (#231) * style: element relationship * feat: utility to add first element * refactor: organise dir * ux: deleting element closes drawer * fix: issue with escaped characters * fix: issue with overflow in events list * fix: prevent event timer from receiving non event * style: prevent small shift on class change * style: visually detach event editor * fix: entry block knows last event * fix: skip event timer updates on non events * fix: prevent electron OPENGL error * style: cleanup text styles * style: unify input styles * refactor: simplify style and composition * refactor: tweaks on style + ts * refactor: add validation to events post * fix: handle non events in event timer * refactor: cleanup unused * V2 styles part2 (#233) * style: create tag to display network messages * style: create button style for playback * style: review style for QuickEntry * style: review style for Info panel * style: review style for Event blocks * style: review style for Message control panel * style: review style for Playback control * style: review application styles * style: review block styles * refactor: TapButton has active state * style: small style tweaks * refactor: type checks * V2 message manager (#234) * refactor: server run script * refactor: extract message control feature * refactor: socket usage for message control feature * refactor: migrate e2e testing to playwright * chore: add feature tests * refactor: add validation on socket controller * chore: jest tests server logic * chore: update tests * chore: run playwright tests on pull_requests * V2 rundown manager (#237) * refactor: migrate eventlist > rundown * hotfix: fix object naming error on excel import (#252) * feat: V2 ontime menu (#259) * V2 socket store (#254) * refactor: create socket utility * refactor: ensure a single subscription to topics * refactor: replace usages of socket object with subscription store handler * refactor: v2 event loader (#260) * refactor: extract jest config * refactor: rename params in reoder endpoint * refactor: avoid app exports * refactor: small code smells * refactor: run development server * refactor: extract rundown service and event loader logic * refactor: rename events > rundown * refactor: code style * fix: handle external change of title * refactor: cleanup dictionary * refactor: rename MessageService * refactor: use eventID for operations * refactor: migrate OSC controller to service * refactor: migrate Socket controller to service * refactor: migrate HTTP controller to service * refactor: extract logic into discrete services * refactor: remove rundown from event timer * refactor: remove duplicate * refactor: remove unused * chore: update tests * refactor: run test CI on master or manual run * lint: reorder imports * refactor: ensure consistent routing * V2 event block (#261) * feat: implement progress bar on running event * refactor: use event action hook * refactor: align backend data * v2 styling (#263) * feat: implement progress bar on running event * refactor: use event action hook * refactor: align backend data * style: v2 style tweaks * style: v2 style tweaks + ts <Info> * fix: fix delay increment calls * style: v2 style tweaks + ts <Playback> * style: v2 style tweaks + ts <EventEditor> * style: v2 style tweaks + ts <MessageControl> * fix: naming issue with message control socket endpoint * chore: upgrade style dependencies * style: v2 style tweaks + ts <MenuBar> * style: v2 style tweaks + ts <RundownMenu> * style: v2 style tweaks + ts <Playback> * fix: add selected and next information to EventTimer * style: v2 style tweaks + ts <EventBlock> * style: v2 style tweaks + ts <DelayBlock> * style: v2 style tweaks + ts <BlockBlock> * style: v2 style tweaks + ts <QuickEntry> * refactor: extract <TextInput> logic * chore: upgrade build dependencies * chore: folder structure refactor * fix: issue with dependency path in electron * chore: update version in demo db * v2: refactor (#264) * chore: upgrade dependencies * chore: remove unused * fix: issue with missing index data * fix: discrepancy on counting events * fix: swapped classes * fix: prevent attempting load in empty list * chore: cleanup completed * chore: remove unused * refactor: convert to typescript * refactor: migrate to new endpoints * refactor: convert to typescript * fix: prevent issue with undefined process * fix: small code smells * fix: issue with missing version variable on build * refactor: configure retries on data fetching * style: design review * fix: prevent cursor out of range * lint: react query linting * style: checkbox styles * v2 event editor (#265) * style: add cursor indicator in all blocks * style: prevent reflows on width * style: harmonise white range * ux: focus elements on cursor nav * ux: prevent focus on indicators * refactor: convert to typescript * fix: missing dependency * chore: add linter rules for dependency arrays * style: style editable component * style: tweak input styles * style: tweak switch styles * style: style tweaks * V2 sentry (#267) * chore: add sentry to frontend * chore: add sentry to server * chore: report only in production * chore: add sentry source maps * v2 alpha 3 (#272) * style: correct background colours * chore: add folder resolutions to vite * refactor: add auto refetch to HTTP APIs * fix: view settings override endpoint * refactor: memoize callbacks * fix: reorder reaching wrong data adapter * refactor: memoize callbacks * style: presentation cleanup * fix(delete): prevent flow with deleted event in playback * refactor: convert to typescript * refactor: small fixes and typescript conversion * ux: improve feedback on local changes * refactor: cleanup props * v2 event timer (#274) * lint: cleanup unused * refactor: rename component to avoid conflict * style: gap in element row * refactor: rename playstate -> playback * refactor: cleanup usages of timer and prepare integration manager * style: re-arrange button order * refactor: improve event loading * feat(timer-service): hot reload * fix: issue with duration input * chore: cleanup debug * refactor: resolve poll from runtime store * refactor: cleanup merge * refactor: small improvements in timer hot-reload * v2: views (#278) * config(sentry): instrumentation only in production * chore: upgrade deps * refactor: simplify and convert to ts * refactor: composition and styles * chore: remove deprecated deps * ux: close menu on click outside * style: small tweaks and clarifications * refactor: retire PiP view * ux: small tweaks * style: cleanup deprecated styles * chore: remove unused * refactor: remove usages of deprecated fields and improve null timer presentation * refactor: improve composition of countdown * style: cleanup overridable styles * ux: show event overtime status * style: correct logic in paused timers * v2: roll (#280) * ux: delete flow * style: small tweaks in interface * refactor: gracefully quit on error * fix: logic around updating events * chore: cleanup dictionary * refactor: improve DX on creating aux files * fix: safe destructure function return * refactor: safe handling of falsy timer values * fix: improve ux on stopping roll mode * chore: upgrade deps * feat: roll mode * refactor: remove unused * V2 monorepo (#285) * refactor(project structure): UI * refactor(project structure): extract utilities * refactor(project structure): remove unused * refactor(project structure): electron * refactor(project structure): server refactor: migrate to vitest refactor: monorepo config * refactor: extract application menu * refactor: exit process * refactor: extract tray menu * chore: electron build * Added Seconds in studio clock #282 --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> --------- Co-authored-by: Fabian Posenau <fabian.p99@gmx.de> Co-authored-by: Fabian Posenau <fabian@fphome.de> * Fix: #279 End Message in Minimal Timer (#286) * fix: Minimal View #279 end message --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * hotfix: resolve data directory (#288) * Feature/#276/qrcodes (#289) * bugfix: wrong format when on 12h format * update: website link in placeholder --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> * V2 types (#291) * V2 integration (#293) * feat: integrations and lifecycles * refactor: prevent issues with start order * V2 fix offline (#294) * fix: resolve path to UI build in dev * fix: configure offline fetching * V2 fix socket (#296) * refactor: rename timer endpoint * refactor: typescript * refactor: rename eventData and viewSettings * refactor: message manager uses event store * style: alpha4 style tweaks (#299) * fix: backgroundcolor for .blockNotes (#300) Co-authored-by: Fabian Posenau <fabian@fphome.de> * Feat/countdown style (#290) * feat: Countdown Style --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> * fix: wrong parameter (#302) Co-authored-by: Fabian Posenau <fabian@fphome.de> * V2 fix timer (#304) * Feat/update check (#303) * feat: show latest version in settings modal --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * improve time input (#307) * add the ability to parse time like 2h20m --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * V2 ws store (#310) * Update TimerService.ts * refactor: message service publishes to store * refactor: several type improvements * V2 ws store wss (#309) * refactor: shared logging types * refactor: simplify message service consumption * refactor: create discrete logging system * refactor: move socket.io > websocket * Feat/end action (#308) * feat: end action --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> * V2 osc (#313) * chore: upgrade relevant deps * fix: electron app to tray * chore: cleanup dictionary * feat: handle several messages in a event * ux: disable irrelevant buttons in browser * feat: parse and validate subscriptions * feat: create UI for OSC Integration * fix: cleanup logger behaviour * feat: allow OSC settings to be changed at runtime * v2 alpha5 (#315) * fix: fetch in offline environments (#295) * add arm platforms to docker build --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * fix: docker build (#298) Co-authored-by: Fabian Posenau <fabian@fphome.de> * Timer: fix too many renders error when using ?progress (#305) * ux: remove duplicate showing of selected event * several small fixes and UX improvements --------- Co-authored-by: Fabian Posenau <fabian.p99@gmx.de> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Marks Polakovs <github@markspolakovs.me> * V2 docker (#312) * Create docker-ci.yml * docker build * add build app * path changes * delete double workflow * update docker run template * cleanup * resolve wierd path issue * wrong path * again wrong path * rename docker releases * move styles population and change to function call * re-add old build pipeline * rename buld file * feractor init function * refactor: startDb to initAssets * refactor: startDB to initAssets * fix: docker.cjs building whole app * fix: docker environment * update docker paths * update: build command * cleanup * cleanup: commented code --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * v2 lazy rundown (#317) * chore: update related deps * refactor: memoise entrypoints * refactor: store selectors * refactor: improve component performance * chore: remove unused dependencies * refactor: improve component dnd * V2 build (#319) * chore: build pipeline for v2 * chore: run unit tests * chore: configure sentry build * Update test_v2.yml * Update test_v2.yml * V2 e2e (#320) * chore: cleanup handling of a bad read * chore: configure e2e testing * chore: version bump * V2 align (#321) * fix: fetch in offline environments (#295) * Add arm platforms to docker build (#297) * add arm platforms to docker build --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * fix: docker build (#298) Co-authored-by: Fabian Posenau <fabian@fphome.de> * Timer: fix too many renders error when using ?progress (#305) * Timer: fix too many renders error when using ?progress * version bump (#314) * style: small tweaks * fix: prevent actions in roll mode * chore: update external assets * style: show shortcuts in quick blocks * refactor: revert optimisations on callbacks * fun: industry dictionary * style: indicators for event features --------- Co-authored-by: Fabian Posenau <fabian.p99@gmx.de> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Marks Polakovs <github@markspolakovs.me> * refactor: ensure start order (#322) * Update build_docker_v2.yml * V2 fix roll (#323) * chore: profile node * fix: always roll * Add translations for views (#325) (#328) * Add translations for views (#325) --------- Co-authored-by: Kevin <60135953+kev-ac@users.noreply.github.com> * feat: notify ws clients on rundown changes (#327) * V2 beta1 (#326) * style: labels on added time * style: remove mentions of PiP * refactor: unify usage of ms for timers * refactor: create events with 0 duration * style: several small tweaks * refactor: keep block when applying delays * feat: blocks have titles * style: improvements in time entry warnings * style: override progress bar styles * style: prevent overflow * feat: show character count in editor * refactor: provide initial payload * refactor: lower test boundary * refactor: get colour from swatches * style: rename title block * chore: version bump * style: fix info scroll * refactor: performance tweaks to event editor * chore: upgrade deps * refactor: prevent iterating on undefined * style: small style tweaks * fix: close editor on event delete * refactor: improve performance on EventBlock * style: small tweaks on blocks * chore: upgrade deps * chore: upgrade deps * style: ensure events are always visible * Fix/events enpoint (#329) * fix: sentry hangs node process * add onClick handler (#335) Co-authored-by: Fabian Posenau <fabian@fphome.de> * fix docker on different port (#337) Co-authored-by: Fabian Posenau <fabian@fphome.de> * V2 beta3 (#338) * chore: upgrade local build documentation * style: keep scrolling event in screen * feat: delay is time entry * refactor: remove unused * refactor: remove unused * refactor: batch store updates * refactor: virtually remove cap on events * refactor: style and behaviour tweaks to event block * style: tweaks on schedules * chore: remove sentry from server * style: reorder menu * chore: update docs * fix: resolve to static port in dev mode * V2 fix delay (#340) * fix: avoid modifying original objects * refactor: serve react as compressed br (#339) * refactor: changes in time input * V2 e2e playwright (#343) * test: create utility for uploading test-db * chore: upgrade deps * refactor: remove unused functions * chore: create testing boilerplate * test: upload showfile * refactor: parse time input (#351) * chore: vite upgrade (#354) * chore: upgrade vite * feat: edit mode (#344) * style: add mode selection to menu * style: rename rundown functions * feat: edit mode * fix: osc modal (#352) * refactor: parse time input * fix: form dirty flag on dynamic elements * style: cuesheet block (#355) * style: cuesheet show block title * style: cuesheet show full delay string * v2 about (#362) * chore: remove unused assets * feat: about ontime * refactor: cleanup unused * v2 beta 4 (#361) * ux: rename end action * chore: update demo db * style: tweaks on extracted rundown * chore: prevent console logs in production code * feat: go mode * style: remove window size limits * style: rename delete action * style: small tweaks on icons * fix: style override on params * chore: update test db * refactor: small code quality improvements * refactor: prevent circular imports * fix v2 roll timer (#363) * fix: roll update skips elapsed time * refactor: remove unused data * style: fix style in roll state * ux: content aware playback (#364) * v2 beta 4 bump (#367) * chore: update links to documentation * chore: version bump * Feature: quick start (#369) * feat: quick start modal * refactor: end message is part of view settings * refactor: small tweaks and type improvements * Feat/timer/stylechange (#341) * feat: multi color progress bar * add color support to minimal timer Co-Authored-By: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> * v2 beta5 modals (#377) * refactor: folder structure * style: modal wrapper * feat: add nordic translations * v2 beta 5 (#375) * chore: update documentation links * fix: sentry has no access to error context * style: clarify event history * fix: issue with clipboard write in safari * style: clarify event history * refactor: improvements to follow logic in rundown * refactor: improvements in go mode * several style tweaks and small improvements * Ux language (#378) * ux: language from menu * feat: add extra support for languages german portuguese spanish * refactor: type improvements * v2 beta6 (#379) * feat: operator key * chore: update demo * chore: version bump * fix: prevent stale keys * refactor: add new fields to validation * style: tweaks to modal arrangement * refactor: prevent parsing http * ux: click anywhere in event to edit * refactor: performance improvements to time input * refactor: performance improvements menu * V2 beta 6 (#381) * fix: fetch in offline environments (#295) * Add arm platforms to docker build (#297) * add arm platforms to docker build --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * fix: docker build (#298) Co-authored-by: Fabian Posenau <fabian@fphome.de> * Timer: fix too many renders error when using ?progress (#305) * Timer: fix too many renders error when using ?progress * version bump (#314) * chore: remove unused assets * refactor: improve rundown loading time * style: rename project file * chore: update demo files * refactor: improve loading time * chore: upgrade upload packages * refactor: modal redesign and ux improvements * refactor: type and ux improvements on translations * refactor: prevent flash on suspense * refactor: remove unnecessary import * chore: update tests * refactor: cleanup imports * fix: prevent flash on suspense --------- Co-authored-by: Fabian Posenau <fabian.p99@gmx.de> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Marks Polakovs <github@markspolakovs.me> * style views (#390) * style: animate transition from left side * refactor: improve schedule relationship * refactor: prevent passing unhandled value * style: remove inner radius in borders --------- Co-authored-by: Fabian Posenau <fabian.p99@gmx.de> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> Co-authored-by: Marks Polakovs <github@markspolakovs.me> Co-authored-by: Kevin <60135953+kev-ac@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`cx() > ignores falsy values 1`] = `""`;
|
||||
|
||||
exports[`cx() > merges styles 1`] = `"_test_98a1e0 _another_98a1e0"`;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { validateAlias } from '../aliases';
|
||||
|
||||
describe('An alias fails if incorrect', () => {
|
||||
const testsToFail = [
|
||||
// no empty
|
||||
'',
|
||||
// no https, http or www
|
||||
'https://www.test.com',
|
||||
'http://www.test.com',
|
||||
'www.test.com',
|
||||
// no hostname
|
||||
'localhost/test',
|
||||
'127.0.0.1/test',
|
||||
'0.0.0.0/test',
|
||||
// no editor
|
||||
'editor',
|
||||
'editor?test',
|
||||
];
|
||||
|
||||
testsToFail.forEach((t) =>
|
||||
it(`${t}`, () => {
|
||||
expect(validateAlias(t).status).toBeFalsy();
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,510 @@
|
||||
import {
|
||||
forgivingStringToMillis,
|
||||
formatDisplay,
|
||||
isTimeString,
|
||||
millisToDelayString,
|
||||
millisToMinutes,
|
||||
millisToSeconds,
|
||||
} from '../dateConfig';
|
||||
|
||||
describe('test string from formatDisplay function', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with not numbers', () => {
|
||||
const t = { val: 'test', result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
const t = { val: -0, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86400 (24 hours)', () => {
|
||||
const t = { val: 86400000, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: '00:00:01' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: '00:00:01' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test string from formatDisplay function with hidezero', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
const t = { val: -0, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86400 (24 hours)', () => {
|
||||
const t = { val: 86400000, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: '00:01' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: '00:01' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test millisToSeconds function', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: 0 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: 3600 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: -3600 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: 0 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
const t = { val: -0, result: -0 };
|
||||
expect(millisToSeconds(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401000 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: 86401 };
|
||||
expect(millisToSeconds(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401000 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: -86401 };
|
||||
expect(millisToSeconds(t.val, false)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test millisToMinutes function', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: 0 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: 60 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: -60 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: 0 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
const t = { val: -0, result: -0 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401000 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: 1440 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401000 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: -1440 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test isTimeString() function', () => {
|
||||
it('it validates time strings', () => {
|
||||
const ts = ['2', '2:10', '2:10:22'];
|
||||
for (const s of ts) {
|
||||
expect(isTimeString(s)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('it fails overloaded times', () => {
|
||||
const ts = ['70', '89:10', '26:10:22'];
|
||||
for (const s of ts) {
|
||||
expect(isTimeString(s)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('test isTimeString() function handle different separators', () => {
|
||||
const ts = ['2:10', '2,10', '2.10'];
|
||||
for (const s of ts) {
|
||||
it(`it handles ${s}`, () => {
|
||||
expect(isTimeString(s)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('test forgivingStringToMillis()', () => {
|
||||
describe('function handles time with no separators', () => {
|
||||
const testData = [
|
||||
{ value: '', expect: 0 },
|
||||
{ value: '0', expect: 0 },
|
||||
{ value: '-0', expect: 0 },
|
||||
{ value: '1', expect: 60 * 1000 },
|
||||
{ value: '-1', expect: 60 * 1000 },
|
||||
{ value: '0h0m0s', expect: 0 },
|
||||
{ value: '0h0m1s', expect: 1000 },
|
||||
{ value: '0h1m0s', expect: 1000 * 60 },
|
||||
{ value: '1h0m0s', expect: 1000 * 60 * 60 },
|
||||
{ value: '23h0m0s', expect: 1000 * 60 * 60 * 23 },
|
||||
{ value: '12h12m12s', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
||||
{ value: '2m', expect: 2 * 60 * 1000 },
|
||||
{ value: '1h5s', expect: 1000 * 60 * 60 + 1000 * 5 },
|
||||
{ value: '1h2m', expect: 1000 * 60 * 60 + 1000 * 60 * 2 },
|
||||
];
|
||||
|
||||
for (const s of testData) {
|
||||
it(`handles ${s.value} to left`, () => {
|
||||
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('parses strings correctly', () => {
|
||||
const ts = [
|
||||
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
|
||||
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
it(`handles ${s.value} to the left`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('parses time strings', () => {
|
||||
const ts = [
|
||||
{ value: '1h2m3s', expect: 60 * 60 * 1000 + 2 * 60 * 1000 + 3 * 1000 },
|
||||
{ value: '1h3s', expect: 60 * 60 * 1000 + 3 * 1000 },
|
||||
{ value: '1h2m', expect: 60 * 60 * 1000 + 2 * 60 * 1000 },
|
||||
{ value: '10h', expect: 10 * 60 * 60 * 1000 },
|
||||
{ value: '10m', expect: 10 * 60 * 1000 },
|
||||
{ value: '10s', expect: 10 * 1000 },
|
||||
{ value: '120h', expect: 120 * 60 * 60 * 1000 },
|
||||
{ value: '120m', expect: 120 * 60 * 1000 },
|
||||
{ value: '120s', expect: 120 * 1000 },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
it(`handles ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('handles am/pm', () => {
|
||||
const ampm = [
|
||||
{ value: '9:10:11am', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||
{ value: '9:10:11a', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||
{ value: '9:10:11pm', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||
{ value: '9:10:11p', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||
{ value: '9:10am', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||
{ value: '9:10a', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||
{ value: '9:10pm', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||
{ value: '9:10p', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||
{ value: '9am', expect: 9 * 60 * 60 * 1000 },
|
||||
{ value: '9a', expect: 9 * 60 * 60 * 1000 },
|
||||
{ value: '9pm', expect: (12 + 9) * 60 * 60 * 1000 },
|
||||
{ value: '9p', expect: (12 + 9) * 60 * 60 * 1000 },
|
||||
{ value: '12am', expect: 0 },
|
||||
{ value: '12pm', expect: 12 * 60 * 60 * 1000 },
|
||||
];
|
||||
|
||||
for (const s of ampm) {
|
||||
it(`handles ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('it infers separators when non existent', () => {
|
||||
const testCases = [
|
||||
{ value: '1', expect: 1000 * 60 }, // 00:01:00
|
||||
{ value: '12', expect: 1000 * 60 * 12 }, // 00:12:00
|
||||
{ value: '123', expect: 1000 * 60 * 23 + 1000 * 60 * 60 }, // 01:23:00
|
||||
{ value: '1234', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 }, // 12:34:00
|
||||
{ value: '12345', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 + 5 * 1000 }, // 12:34:05
|
||||
{ value: '123456', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 + 56 * 1000 }, // 12:34:56
|
||||
];
|
||||
|
||||
for (const s of testCases) {
|
||||
it(`handles basic strings digits: ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
|
||||
const sixDigits = [
|
||||
{ value: '000000', expect: 0 },
|
||||
{ value: '000001', expect: 1000 },
|
||||
{ value: '000100', expect: 1000 * 60 },
|
||||
{ value: '010000', expect: 1000 * 60 * 60 },
|
||||
{ value: '230000', expect: 1000 * 60 * 60 * 23 },
|
||||
{ value: '121212', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
||||
];
|
||||
|
||||
for (const s of sixDigits) {
|
||||
it(`handles string with 6 digits: ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
|
||||
const fiveDigits = [
|
||||
{ value: '00000', expect: 0 },
|
||||
{ value: '00001', expect: 1000 }, // 00:00:01
|
||||
{ value: '00010', expect: 1000 * 60 }, // 00:01:00
|
||||
{ value: '00100', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||
{ value: '01000', expect: 1000 * 60 * 60 }, // 01:00:00
|
||||
{ value: '10000', expect: 1000 * 60 * 60 * 10 }, // 10:00:00
|
||||
{ value: '23000', expect: 1000 * 60 * 60 * 23 }, // 23:00:00
|
||||
{ value: '12121', expect: 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, // 12:12:01
|
||||
];
|
||||
|
||||
for (const s of fiveDigits) {
|
||||
it(`handles string with 5 digits: ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
|
||||
const fourDigits = [
|
||||
{ value: '0000', expect: 0 },
|
||||
{ value: '0001', expect: 1000 * 60 }, // 00:01:00
|
||||
{ value: '0010', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||
{ value: '0100', expect: 1000 * 60 * 60 }, // 01:00:00
|
||||
{ value: '1000', expect: 1000 * 60 * 60 * 10 }, // 10:00:00
|
||||
{ value: '2300', expect: 1000 * 60 * 60 * 23 }, // 23:00:00
|
||||
{ value: '1212', expect: 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, // 12:12:00
|
||||
];
|
||||
|
||||
for (const s of fourDigits) {
|
||||
it(`handles string with 4 digits: ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
|
||||
const threeDigits = [
|
||||
{ value: '000', expect: 0 },
|
||||
{ value: '001', expect: 1000 * 60 }, // 00:01:00
|
||||
{ value: '010', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||
{ value: '100', expect: 1000 * 60 * 60 }, // 01:00:00
|
||||
{ value: '230', expect: 2 * 1000 * 60 * 60 + 30 * 1000 * 60 }, // 02:30:00
|
||||
{ value: '121', expect: 21 * 60 * 1000 + 1000 * 60 * 60 }, // 01:21:00
|
||||
];
|
||||
|
||||
for (const s of threeDigits) {
|
||||
it(`handles string with 3 digits: ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
|
||||
const twoDigits = [
|
||||
{ value: '00', expect: 0 },
|
||||
{ value: '01', expect: 1000 * 60 }, // 00:01:00
|
||||
{ value: '10', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||
{ value: '23', expect: 1000 * 60 * 23 }, // 00:23:00
|
||||
];
|
||||
|
||||
for (const s of twoDigits) {
|
||||
it(`handles string with 2 digits: ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
|
||||
const singleDigit = [...Array(10).keys()];
|
||||
|
||||
for (const s of singleDigit) {
|
||||
it(`handles string with a single digits ${s}`, () => {
|
||||
expect(forgivingStringToMillis(`${s}`)).toBe(s * 1000 * 60);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('handles overflows', () => {
|
||||
const ts = [
|
||||
// minutes overflow
|
||||
{ value: '2.0.0', expect: 1000 * 60 * 120 },
|
||||
{ value: '99', expect: 1000 * 60 * 99 },
|
||||
{ value: '1.39.0', expect: 1000 * 60 * 99 },
|
||||
// seconds overflow
|
||||
{ value: '0.0.120', expect: 120 * 1000 },
|
||||
{ value: '0.2.0', expect: 120 * 1000 },
|
||||
{ value: '0.0.99', expect: 99 * 1000 },
|
||||
{ value: '0.1.39', expect: 99 * 1000 },
|
||||
// hours overflow
|
||||
{ value: '25.0.0', expect: 1000 * 60 * 60 * 25 },
|
||||
// hours overflow
|
||||
{ value: '50.0.0', expect: 1000 * 60 * 60 * 50 },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
it(`handles ${s.value} to the left`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('test fillLeft', () => {
|
||||
describe('function handles separators', () => {
|
||||
const testData = [
|
||||
{ value: '1:2:3:10', expect: 3723000 },
|
||||
{ value: '2,10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 },
|
||||
{ value: '2.10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 },
|
||||
{ value: '2 10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 },
|
||||
];
|
||||
|
||||
for (const s of testData) {
|
||||
it(`handles ${s.value}`, () => {
|
||||
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('parses strings correctly', () => {
|
||||
const ts = [
|
||||
{ value: '1.2', expect: 60 * 60 * 1000 + 2 * 60 * 1000 },
|
||||
{ value: '1.70', expect: 60 * 60 * 1000 + 70 * 60 * 1000 },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
it(`handles ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('handles overflows', () => {
|
||||
const ts = [
|
||||
// minutes overflow
|
||||
{ value: '0.120', expect: 120 * 60 * 1000 },
|
||||
{ value: '0.99', expect: 99 * 60 * 1000 },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
it(`handles ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('millisToDelayString()', () => {
|
||||
it('returns null for null values', () => {
|
||||
expect(millisToDelayString(null)).toBeNull();
|
||||
});
|
||||
it('returns null 0', () => {
|
||||
expect(millisToDelayString(0)).toBeNull();
|
||||
});
|
||||
describe('converts values in seconds', () => {
|
||||
it('shows a simple string with value in seconds', () => {
|
||||
expect(millisToDelayString(10000)).toBe('+10 sec');
|
||||
});
|
||||
it('... and its negative counterpart', () => {
|
||||
expect(millisToDelayString(-10000)).toBe('-10 sec');
|
||||
});
|
||||
|
||||
const underAMinute = [1, 500, 1000, 6000, 55000, 59999];
|
||||
underAMinute.forEach((value) => {
|
||||
it(`handles ${value}`, () => {
|
||||
expect(millisToDelayString(value)?.endsWith('sec')).toBe(true);
|
||||
});
|
||||
});
|
||||
expect(millisToDelayString(null)).toBeNull();
|
||||
});
|
||||
|
||||
describe('converts values in minutes', () => {
|
||||
it('shows a simple string with value in minutes', () => {
|
||||
expect(millisToDelayString(720000)).toBe('+12 min');
|
||||
});
|
||||
it('... and its negative counterpart', () => {
|
||||
expect(millisToDelayString(-720000)).toBe('-12 min');
|
||||
});
|
||||
it('shows a simple string with value in minutes and seconds', () => {
|
||||
expect(millisToDelayString(630000)).toBe('+00:10:30');
|
||||
});
|
||||
it('... and its negative counterpart', () => {
|
||||
expect(millisToDelayString(-630000)).toBe('-00:10:30');
|
||||
});
|
||||
|
||||
const underAnHour = [60000, 360000, 720000];
|
||||
underAnHour.forEach((value) => {
|
||||
it(`handles ${value}`, () => {
|
||||
expect(millisToDelayString(value)?.endsWith('min')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('converts values with full time string', () => {
|
||||
it('positive added time', () => {
|
||||
expect(millisToDelayString(45015000)).toBe('+12:30:15');
|
||||
});
|
||||
it('negative added time', () => {
|
||||
expect(millisToDelayString(-45015000)).toBe('-12:30:15');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,536 @@
|
||||
import { formatEventList, getEventsWithDelay, trimRundown } from '../eventsManager';
|
||||
|
||||
describe('getEventsWithDelay function', () => {
|
||||
test('with positive delays', () => {
|
||||
const testData = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
duration: 60000,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000,
|
||||
timeEnd: 35520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
{
|
||||
title: 'Use simpler times to create a timer',
|
||||
timeStart: 120000,
|
||||
timeEnd: 720000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8222',
|
||||
},
|
||||
{
|
||||
duration: 900000,
|
||||
type: 'delay',
|
||||
revision: 0,
|
||||
id: 'a386',
|
||||
},
|
||||
{
|
||||
title: 'Add delay blocks to affect all events',
|
||||
timeStart: 37320000,
|
||||
timeEnd: 38520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '6dce',
|
||||
},
|
||||
{
|
||||
title: 'Add and remove events with [+] and [-]',
|
||||
timeStart: 38520000,
|
||||
timeEnd: 45120000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '2651',
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'e6a1',
|
||||
},
|
||||
{
|
||||
title: 'And control whether they are public',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '1358',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000 + 60000,
|
||||
timeEnd: 35520000 + 60000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
{
|
||||
title: 'Use simpler times to create a timer',
|
||||
timeStart: 120000 + 60000,
|
||||
timeEnd: 720000 + 60000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8222',
|
||||
},
|
||||
{
|
||||
title: 'Add delay blocks to affect all events',
|
||||
timeStart: 37320000 + 60000 + 900000,
|
||||
timeEnd: 38520000 + 60000 + 900000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '6dce',
|
||||
},
|
||||
{
|
||||
title: 'Add and remove events with [+] and [-]',
|
||||
timeStart: 38520000 + 60000 + 900000,
|
||||
timeEnd: 45120000 + 60000 + 900000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '2651',
|
||||
},
|
||||
{
|
||||
title: 'And control whether they are public',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '1358',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
test('with negative delays', () => {
|
||||
const testData = [
|
||||
{
|
||||
duration: -20,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 100,
|
||||
timeEnd: 200,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 80,
|
||||
timeEnd: 180,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventsWithDelay edge cases', () => {
|
||||
it('ensures time start cannot be below 0', () => {
|
||||
const testData = [
|
||||
{
|
||||
duration: -200,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 0,
|
||||
timeEnd: 0,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
it('does not modify original array', () => {
|
||||
const testData = [
|
||||
{
|
||||
duration: 10,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 20,
|
||||
timeEnd: 30,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
const expectedSafe = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 20,
|
||||
timeEnd: 30,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
expect(getEventsWithDelay(expectedSafe)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('given an empty array', () => {
|
||||
const emptyArray = {
|
||||
test: [],
|
||||
expect: [],
|
||||
};
|
||||
|
||||
expect(getEventsWithDelay(emptyArray.test)).toStrictEqual(emptyArray.expect);
|
||||
});
|
||||
|
||||
it('given an undefined object', () => {
|
||||
const withUndefined = {
|
||||
test: undefined,
|
||||
expect: [],
|
||||
};
|
||||
|
||||
expect(getEventsWithDelay(withUndefined.test)).toStrictEqual(withUndefined.expect);
|
||||
});
|
||||
|
||||
it('given a corrupted event object', () => {
|
||||
const testData = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
duration: 60000,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000,
|
||||
timeEnd: 35520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000 + 60000,
|
||||
timeEnd: 35520000 + 60000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('given a corrupted delay object', () => {
|
||||
const testData = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000,
|
||||
timeEnd: 35520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000,
|
||||
timeEnd: 35520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test trimEventlist function', () => {
|
||||
const limit = 8;
|
||||
const testData = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' },
|
||||
{ id: '10' },
|
||||
{ id: '11' },
|
||||
{ id: '12' },
|
||||
];
|
||||
|
||||
it('when we use the first item', () => {
|
||||
const selectedId = '1';
|
||||
const expected = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData, selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('when we use the third item', () => {
|
||||
const selectedId = '3';
|
||||
const expected = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData, selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('when we use the fourth item', () => {
|
||||
const selectedId = '4';
|
||||
const expected = [
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData, selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if selected is not found', () => {
|
||||
const selectedId = '15';
|
||||
const expected = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData, selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test formatEvents function', () => {
|
||||
const testEvent = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
subtitle: 'Subtitles are useful',
|
||||
presenter: 'cpvalente',
|
||||
note: 'Maybe a running note for the operator?',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
isPublic: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: 'In green, below',
|
||||
timeStart: 34800000,
|
||||
timeEnd: 35400000,
|
||||
isPublic: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
|
||||
it('it parses correctly', () => {
|
||||
const selectedId = 'otherEvent';
|
||||
const nextId = 'notHere';
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('it handles selected correctly', () => {
|
||||
const selectedId = '5946';
|
||||
const nextId = '8ee5';
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: true,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('it handles next correctly', () => {
|
||||
const selectedId = '8ee5';
|
||||
const nextId = 'notHere';
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import getDelayTo from '../getDelayTo';
|
||||
|
||||
describe('getDelayTo function', () => {
|
||||
it('handles list with delays', () => {
|
||||
const delayDuration = 100;
|
||||
const events = [
|
||||
{ type: 'event' },
|
||||
{ type: 'delay', duration: delayDuration },
|
||||
{ type: 'event' },
|
||||
];
|
||||
|
||||
const notDelayed = getDelayTo(events, 0);
|
||||
expect(notDelayed).toBe(0);
|
||||
const delayedEvent = getDelayTo(events, 2);
|
||||
expect(delayedEvent).toBe(delayDuration);
|
||||
});
|
||||
it('handles list without delays', () => {
|
||||
const events = [{ type: 'event' }, { type: 'event' }];
|
||||
const notDelayed = getDelayTo(events, 1);
|
||||
expect(notDelayed).toBe(0);
|
||||
});
|
||||
|
||||
it('handles list with multiple delays', () => {
|
||||
const delayDuration = 100;
|
||||
const events = [
|
||||
{ type: 'event' },
|
||||
{ type: 'delay', duration: delayDuration },
|
||||
{ type: 'event' },
|
||||
{ type: 'delay', duration: delayDuration },
|
||||
{ type: 'event' },
|
||||
];
|
||||
const doubleDelay = getDelayTo(events, 4);
|
||||
expect(doubleDelay).toBe(delayDuration * 2);
|
||||
});
|
||||
it('handles list with blocks', () => {
|
||||
const events = [
|
||||
{ type: 'event' },
|
||||
{ type: 'delay', duration: 100 },
|
||||
{ type: 'event' },
|
||||
{ type: 'block' },
|
||||
{ type: 'event' },
|
||||
];
|
||||
const notDelayed = getDelayTo(events, 4);
|
||||
expect(notDelayed).toBe(0);
|
||||
});
|
||||
it('handles index greater than list', () => {
|
||||
const events = [{ type: 'event' }, { type: 'delay', duration: 100 }, { type: 'event' }];
|
||||
const notDelayed = getDelayTo(events, 3);
|
||||
expect(notDelayed).toBe(0);
|
||||
});
|
||||
it('handles negative index (not found)', () => {
|
||||
const events = [{ type: 'event' }, { type: 'delay', duration: 100 }, { type: 'event' }];
|
||||
const notDelayed = getDelayTo(events, -1);
|
||||
expect(notDelayed).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { clamp } from '../math';
|
||||
|
||||
test('Clamps a set of numbers correctly', () => {
|
||||
const testCases = [
|
||||
{ num: 10, min: 0, max: 20, result: 10 },
|
||||
{ num: 0, min: 0, max: 20, result: 0 },
|
||||
{ num: 20, min: 0, max: 20, result: 20 },
|
||||
{ num: 20, min: 0, max: 20, result: 20 },
|
||||
{ num: -20, min: 0, max: 20, result: 0 },
|
||||
{ num: -0, min: 0, max: 20, result: 0 },
|
||||
{ num: -50, min: -30, max: -20, result: -30 },
|
||||
{ num: -50, min: 0, max: 0, result: 0 },
|
||||
{ num: 50.5, min: 0, max: 100, result: 50.5 },
|
||||
{ num: 50, min: 0, max: 20.32, result: 20.32 },
|
||||
{ num: 10, min: 20.32, max: 40, result: 20.32 }
|
||||
];
|
||||
|
||||
testCases.forEach((t) => expect(clamp(t.num, t.min, t.max)).toBe(t.result));
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { isIPAddress, isOnlyNumbers } from '../regex';
|
||||
|
||||
describe('simple tests for regex', () => {
|
||||
test('isOnlyNumbers', () => {
|
||||
const right = ['1231', '1'];
|
||||
const wrong = ['a', 'asdas1asdas', '11as', '1_', '1.1'];
|
||||
|
||||
right.forEach((t) => {
|
||||
expect(isOnlyNumbers.test(t)).toBe(true);
|
||||
});
|
||||
wrong.forEach((t) => {
|
||||
expect(isOnlyNumbers.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('isIPAddress', () => {
|
||||
const right = ['0.0.0.0', '127.0.0.1'];
|
||||
const wrong = ['0', 'testing', '123.0.1'];
|
||||
|
||||
right.forEach((t) => {
|
||||
expect(isIPAddress.test(t)).toBe(true);
|
||||
});
|
||||
wrong.forEach((t) => {
|
||||
expect(isIPAddress.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
.test {}
|
||||
.another {}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { cx } from '../styleUtils';
|
||||
|
||||
import style from './styleUtils.module.scss';
|
||||
|
||||
describe('cx()', () => {
|
||||
it('merges styles', () => {
|
||||
const merged = cx([style.test, style.another]);
|
||||
expect(merged).toMatchSnapshot();
|
||||
});
|
||||
it('ignores falsy values', () => {
|
||||
const falsyStuff = false;
|
||||
const merged = cx([undefined, false, 0, null, falsyStuff ? style.test : null]);
|
||||
expect(merged).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { formatTime } from '../time';
|
||||
|
||||
describe('formatTime()', () => {
|
||||
it('parses 24h strings', () => {
|
||||
const ms = 13 * 60 * 60 * 1000;
|
||||
const options = {
|
||||
showSeconds: true,
|
||||
format: 'irrelevant',
|
||||
};
|
||||
const time = formatTime(ms, options, () => '24');
|
||||
expect(time).toStrictEqual('13:00:00');
|
||||
});
|
||||
|
||||
it('parses same string in 12h strings', () => {
|
||||
const ms = 13 * 60 * 60 * 1000;
|
||||
const options = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
const time = formatTime(ms, options, () => '12');
|
||||
expect(time).toStrictEqual('01:00:00 PM');
|
||||
});
|
||||
|
||||
it('handles null times', () => {
|
||||
const ms = null;
|
||||
const time = formatTime(ms);
|
||||
expect(time).toStrictEqual('...');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { calculateDuration, DAY_TO_MS } from '../timesManager';
|
||||
|
||||
describe('calculateDuration()', () => {
|
||||
describe('Given start and end values', () => {
|
||||
it('calculates duration correctly', () => {
|
||||
const testStart = 1;
|
||||
const testEnd = 2;
|
||||
const val = calculateDuration(testStart, testEnd);
|
||||
expect(val).toBe(testEnd - testStart);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Handles edge cases', () => {
|
||||
it('when start is after end', () => {
|
||||
const testStart = 3;
|
||||
const testEnd = 2;
|
||||
const val = calculateDuration(testStart, testEnd);
|
||||
expect(val).toBe(testEnd + DAY_TO_MS - testStart);
|
||||
});
|
||||
it('when both are equal', () => {
|
||||
const testStart = 1;
|
||||
const testEnd = 1;
|
||||
const val = calculateDuration(testStart, testEnd);
|
||||
expect(val).toBe(testEnd - testStart);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Validates an alias against defined parameters
|
||||
* @param {string} alias
|
||||
* @returns {{message: string, status: boolean}}
|
||||
*/
|
||||
export const validateAlias = (alias: string) => {
|
||||
|
||||
const valid = { status: true, message: 'ok' };
|
||||
|
||||
if (alias === '' || alias == null) {
|
||||
// cannot be empty
|
||||
valid.status = false;
|
||||
valid.message = 'should not be empty';
|
||||
} else if (alias.includes('http') || alias.includes('https') || alias.includes('www')) {
|
||||
// cannot contain http, https or www
|
||||
valid.status = false;
|
||||
valid.message = 'should not include http, https, www';
|
||||
} else if (alias.includes('127.0.0.1') || alias.includes('localhost') || alias.includes('0.0.0.0')) {
|
||||
// aliases cannot contain hostname
|
||||
valid.status = false;
|
||||
valid.message = 'should not include hostname';
|
||||
} else if (alias.includes('editor')) {
|
||||
// no editor
|
||||
valid.status = false;
|
||||
valid.message = 'No aliases to editor page allowed';
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
@@ -0,0 +1,242 @@
|
||||
import { formatFromMillis } from 'ontime-utils';
|
||||
|
||||
import { mth, mtm, mts } from './timeConstants';
|
||||
|
||||
export const timeFormat = 'HH:mm';
|
||||
export const timeFormatSeconds = 'HH:mm:ss';
|
||||
|
||||
/**
|
||||
* another go at simpler string formatting (counters)
|
||||
* @description Converts seconds to string representing time
|
||||
* @param {number | null} milliseconds - time in seconds
|
||||
* @param {boolean} [hideZero] - whether to show hours in case its 00
|
||||
* @returns {string} String representing absolute time 00:12:02
|
||||
*/
|
||||
export function formatDisplay(milliseconds: number | null, hideZero = false): string {
|
||||
if (typeof milliseconds !== 'number') {
|
||||
return hideZero ? '00:00' : '00:00:00';
|
||||
}
|
||||
|
||||
// add an extra 0 if necessary
|
||||
const format = (val: number) => `0${Math.floor(val)}`.slice(-2);
|
||||
|
||||
const s = Math.abs(millisToSeconds(milliseconds));
|
||||
const hours = Math.floor((s / 3600) % 24);
|
||||
const minutes = Math.floor((s % 3600) / 60);
|
||||
|
||||
if (hideZero && hours < 1) return [minutes, s % 60].map(format).join(':');
|
||||
return [hours, minutes, s % 60].map(format).join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Converts milliseconds to seconds
|
||||
* @param {number | null} millis - time in seconds
|
||||
* @returns {number} Amount in seconds
|
||||
*/
|
||||
export const millisToSeconds = (millis: number | null): number => {
|
||||
if (millis === null) {
|
||||
return 0;
|
||||
}
|
||||
return millis < 0 ? Math.ceil(millis / mts) : Math.floor(millis / mts);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts milliseconds to seconds
|
||||
* @param {number} millis - time in milliseconds
|
||||
* @returns {number} Amount in seconds
|
||||
*/
|
||||
export const millisToMinutes = (millis: number): number => {
|
||||
return millis < 0 ? Math.ceil(millis / mtm) : Math.floor(millis / mtm);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Validates a time string
|
||||
* @param {string} string - time string "23:00:12"
|
||||
* @returns {boolean} string represents time
|
||||
*/
|
||||
export const isTimeString = (string: string): boolean => {
|
||||
// ^ # Start of string
|
||||
// (?: # Try to match...
|
||||
// (?: # Try to match...
|
||||
// ([01]?\d|2[0-3]): # HH:
|
||||
// )? # (optionally).
|
||||
// ([0-5]?\d): # MM: (required)
|
||||
// )? # (entire group optional, so either HH:MM:, MM: or nothing)
|
||||
// ([0-5]?\d) # SS (required)
|
||||
// $ # End of string
|
||||
|
||||
const regex = /^(?:(?:([01]?\d|2[0-3])[:,.])?([0-5]?\d)[:,.])?([0-5]?\d)$/;
|
||||
return regex.test(string);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description safe parse string to int
|
||||
* @param {string} valueAsString
|
||||
* @return {number}
|
||||
*/
|
||||
const parse = (valueAsString: string): number => {
|
||||
const parsed = parseInt(valueAsString, 10);
|
||||
if (isNaN(parsed)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.abs(parsed);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Utility function to check if a string contain am/pm indicators
|
||||
* @param {string} value
|
||||
*/
|
||||
function checkAmPm(value: string) {
|
||||
let isPM = false;
|
||||
let isAM = false;
|
||||
if (value.toLowerCase().includes('pm')) {
|
||||
isPM = true;
|
||||
value = value.replace(/pm/i, '');
|
||||
} else if (value.toLowerCase().includes('p')) {
|
||||
isPM = true;
|
||||
value = value.replace(/p/i, '');
|
||||
}
|
||||
|
||||
// we need to remove am, but it doesn't actually change anything
|
||||
if (value.toLowerCase().includes('am')) {
|
||||
isAM = true;
|
||||
value = value.replace(/am/i, '');
|
||||
} else if (value.toLowerCase().includes('a')) {
|
||||
isAM = true;
|
||||
value = value.replace(/a/i, '');
|
||||
}
|
||||
|
||||
return { isAM, isPM, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Utility function to check if a string contain h / m / s indicators
|
||||
* @param {string} value
|
||||
*/
|
||||
function checkMatchers(value: string) {
|
||||
const hoursMatch = /(\d+)h/.exec(value);
|
||||
const hoursMatchValue = hoursMatch ? parse(hoursMatch[1]) : 0;
|
||||
|
||||
const minutesMatch = /(\d+)m/.exec(value);
|
||||
const minutesMatchValue = minutesMatch ? parse(minutesMatch[1]) : 0;
|
||||
|
||||
const secondsMatch = /(\d+)s/.exec(value);
|
||||
const secondsMatchValue = secondsMatch ? parse(secondsMatch[1]) : 0;
|
||||
|
||||
if (hoursMatchValue > 0 || minutesMatchValue > 0 || secondsMatchValue > 0) {
|
||||
return hoursMatchValue * mth + minutesMatchValue * mtm + secondsMatchValue * mts;
|
||||
}
|
||||
return { hoursMatchValue };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Utility function to infer separators from a whole string
|
||||
* @param {string} value
|
||||
* @param {boolean} isAM
|
||||
* @param {boolean} isPM
|
||||
*/
|
||||
function inferSeparators(value: string, isAM: boolean, isPM: boolean) {
|
||||
const length = value.length;
|
||||
let inferredMillis = 0;
|
||||
let addAM = 0;
|
||||
if (length === 1) {
|
||||
if (isPM || isAM) {
|
||||
inferredMillis = parse(value) * mth;
|
||||
if (isAM) {
|
||||
// this ensures we dont add 12 hours in the end
|
||||
addAM = inferredMillis;
|
||||
}
|
||||
} else {
|
||||
inferredMillis = parse(value) * mtm;
|
||||
}
|
||||
} else if (length === 2) {
|
||||
if (isPM || isAM) {
|
||||
inferredMillis = parse(value) * inferredMillis;
|
||||
if (isAM) {
|
||||
// this ensures we dont add 12 hours in the end
|
||||
addAM = 12;
|
||||
}
|
||||
} else {
|
||||
inferredMillis = parse(value) * mtm;
|
||||
}
|
||||
} else if (length === 3) {
|
||||
inferredMillis = parse(value[0]) * mth + parse(value.substring(1)) * mtm;
|
||||
} else if (length === 4) {
|
||||
inferredMillis = parse(value.substring(0, 2)) * mth + parse(value.substring(2)) * mtm;
|
||||
} else if (length === 5) {
|
||||
const hours = parse(value.substring(0, 2));
|
||||
const minutes = parse(value.substring(2, 4));
|
||||
const seconds = parse(value.substring(4));
|
||||
inferredMillis = hours * mth + minutes * mtm + seconds * mts;
|
||||
} else if (length >= 6) {
|
||||
const hours = parse(value.substring(0, 2));
|
||||
const minutes = parse(value.substring(2, 4));
|
||||
const seconds = parse(value.substring(4));
|
||||
inferredMillis = hours * mth + minutes * mtm + seconds * mts;
|
||||
}
|
||||
return { inferredMillis, addAM };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Parses a time string to millis, auto-filling to the left
|
||||
* @param {string} value - time string
|
||||
* @returns {number} - time string in millis
|
||||
*/
|
||||
export const forgivingStringToMillis = (value: string): number => {
|
||||
if (value === '12am') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const { isAM, isPM, value: parsingValue } = checkAmPm(value);
|
||||
const maybeMillisFromMatchers = checkMatchers(parsingValue);
|
||||
if (typeof maybeMillisFromMatchers === 'number') {
|
||||
return maybeMillisFromMatchers;
|
||||
}
|
||||
|
||||
let { hoursMatchValue } = maybeMillisFromMatchers;
|
||||
|
||||
let millis = 0;
|
||||
|
||||
// split string at known separators : , .
|
||||
const separatorRegex = /[\s,:.]+/;
|
||||
const [first, second, third] = parsingValue.split(separatorRegex);
|
||||
|
||||
if (first != null && second != null && third != null) {
|
||||
// if string has three sections, treat as [hours] [minutes] [seconds]
|
||||
millis = parse(first) * mth;
|
||||
millis += parse(second) * mtm;
|
||||
millis += parse(third) * mts;
|
||||
} else if (first != null && second == null && third == null) {
|
||||
// we only have one section, infer separators
|
||||
const { inferredMillis, addAM } = inferSeparators(first, isAM, isPM);
|
||||
millis = inferredMillis;
|
||||
hoursMatchValue = addAM;
|
||||
}
|
||||
if (first != null && second != null && third == null) {
|
||||
millis = parse(first) * mth;
|
||||
millis += parse(second) * mtm;
|
||||
}
|
||||
|
||||
// Add 12 hours if it is PM
|
||||
if (isPM && hoursMatchValue < 12) {
|
||||
millis += 12 * mth;
|
||||
}
|
||||
return millis;
|
||||
};
|
||||
|
||||
export function millisToDelayString(millis: number | null): undefined | string | null {
|
||||
if (millis == null || millis === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isNegative = millis < 0;
|
||||
const absMillis = Math.abs(millis);
|
||||
|
||||
if (absMillis < mtm) {
|
||||
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 's')} sec`;
|
||||
} else if (absMillis < mth && absMillis % mtm === 0) {
|
||||
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'm')} min`;
|
||||
} else {
|
||||
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'HH:mm:ss')}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { OntimeEvent, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { formatTime } from './time';
|
||||
|
||||
/**
|
||||
* @description From a list of events, returns only events of type event with calculated delays
|
||||
* @param {Object[]} rundown - given rundown
|
||||
* @returns {Object[]} Filtered events with calculated delays
|
||||
*/
|
||||
|
||||
export const getEventsWithDelay = (rundown: OntimeRundownEntry[]): OntimeEvent[] => {
|
||||
if (rundown == null) return [];
|
||||
|
||||
const delayedEvents: OntimeEvent[] = [];
|
||||
|
||||
// Add running delay
|
||||
let delay = 0;
|
||||
for (const event of rundown) {
|
||||
if (event.type === SupportedEvent.Block) delay = 0;
|
||||
else if (event.type === SupportedEvent.Delay) {
|
||||
if (typeof event.duration === 'number') {
|
||||
delay += event.duration;
|
||||
}
|
||||
} else if (event.type === SupportedEvent.Event) {
|
||||
const delayedEvent = { ...event };
|
||||
if (delay !== 0) {
|
||||
delayedEvent.timeStart = Math.max(delayedEvent.timeStart + delay, 0);
|
||||
delayedEvent.timeEnd = Math.max(delayedEvent.timeEnd + delay, 0);
|
||||
}
|
||||
delayedEvents.push(delayedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return delayedEvents;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Returns trimmed event list array
|
||||
* @param {Object[]} rundown - given rundown
|
||||
* @param {string} selectedId - id of currently selected event
|
||||
* @param {number} limit - max number of events to return
|
||||
* @returns {Object[]} Event list with maximum <limit> objects
|
||||
*/
|
||||
export const trimRundown = (rundown: OntimeRundownEntry[], selectedId: string, limit: number) => {
|
||||
if (rundown == null) return [];
|
||||
|
||||
const BEFORE = 2;
|
||||
const trimmedRundown = [...rundown];
|
||||
|
||||
// limit events length if necessary
|
||||
if (limit != null) {
|
||||
while (trimmedRundown.length > limit) {
|
||||
const idx = trimmedRundown.findIndex((e) => e.id === selectedId);
|
||||
if (idx <= BEFORE) {
|
||||
trimmedRundown.pop();
|
||||
} else {
|
||||
trimmedRundown.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
return trimmedRundown;
|
||||
};
|
||||
|
||||
type FormatEventListOptionsProp = {
|
||||
showEnd?: boolean;
|
||||
};
|
||||
/**
|
||||
* @description Returns list of events formatted to be displayed
|
||||
* @param {Object[]} rundown - given rundown
|
||||
* @param {string} selectedId - id of currently selected event
|
||||
* @param {string} nextId - id of next event
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.showEnd] - whether to show the end time
|
||||
* @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}]
|
||||
*/
|
||||
export const formatEventList = (
|
||||
rundown: OntimeEvent[],
|
||||
selectedId: string,
|
||||
nextId: string,
|
||||
options: FormatEventListOptionsProp,
|
||||
) => {
|
||||
if (rundown == null) return [];
|
||||
const { showEnd = false } = options;
|
||||
|
||||
const givenEvents = [...rundown];
|
||||
|
||||
// format list
|
||||
const formattedEvents = [];
|
||||
for (const event of givenEvents) {
|
||||
const start = formatTime(event.timeStart);
|
||||
const end = formatTime(event.timeEnd);
|
||||
|
||||
formattedEvents.push({
|
||||
id: event.id,
|
||||
time: showEnd ? `${start} - ${end}` : start,
|
||||
title: event.title,
|
||||
isNow: event.id === selectedId,
|
||||
isNext: event.id === nextId,
|
||||
colour: event.colour,
|
||||
});
|
||||
}
|
||||
|
||||
return formattedEvents;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Creates a safe duplicate of an event
|
||||
* @param {object} event
|
||||
* @return {object} clean event
|
||||
*/
|
||||
type ClonedEvent = OntimeEvent | { after?: string };
|
||||
export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
||||
return {
|
||||
type: SupportedEvent.Event,
|
||||
title: event.title,
|
||||
subtitle: event.subtitle,
|
||||
presenter: event.presenter,
|
||||
note: event.note,
|
||||
timeStart: event.timeStart,
|
||||
timeEnd: event.timeEnd,
|
||||
isPublic: event.isPublic,
|
||||
skip: event.skip,
|
||||
colour: event.colour,
|
||||
after: after,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets first event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @return {OntimeEvent | null}
|
||||
*/
|
||||
export function getFirstEvent(rundown: OntimeRundownEntry[]) {
|
||||
return rundown.length ? rundown[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets next event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @param {string} currentId
|
||||
* @return {OntimeEvent | null}
|
||||
*/
|
||||
export function getNextEvent(rundown: OntimeRundownEntry[], currentId: string) {
|
||||
const index = rundown.findIndex((event) => event.id === currentId);
|
||||
if (index !== -1 && index + 1 < rundown.length) {
|
||||
return rundown[index + 1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previous event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @param {string} currentId
|
||||
* @return {OntimeEvent | null}
|
||||
*/
|
||||
export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: string) {
|
||||
const index = rundown.findIndex((event) => event.id === currentId);
|
||||
if (index !== -1 && index - 1 >= 0) {
|
||||
return rundown[index - 1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @description calculates delay to a given event
|
||||
* @param {array} events
|
||||
* @param {number} eventIndex
|
||||
* @return {number} - delay value of given event
|
||||
*/
|
||||
export default function getDelayTo(events, eventIndex) {
|
||||
let delay = 0;
|
||||
let index = 0;
|
||||
if (eventIndex >= 0) {
|
||||
for (const event of events) {
|
||||
if (eventIndex === index) {
|
||||
return delay;
|
||||
}
|
||||
|
||||
if (event.type === 'delay') {
|
||||
delay += event.duration;
|
||||
} else if (event.type === 'block') {
|
||||
delay = 0;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Returns hostname
|
||||
* @type {string}
|
||||
*/
|
||||
export const host = window?.location?.host;
|
||||
|
||||
/**
|
||||
* Open an external URLs: specifically for a electron / browser case
|
||||
* If electron: ask main process to call a new browser window
|
||||
* If browser: open in new tab
|
||||
* @param url
|
||||
*/
|
||||
export function openLink(url) {
|
||||
if (window.process?.type === 'renderer') {
|
||||
window.ipcRenderer.send('send-to-link', url);
|
||||
} else {
|
||||
window.open(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles opening external links
|
||||
* @param event
|
||||
* @param location
|
||||
*/
|
||||
export function handleLinks(event, location) {
|
||||
// we handle the link manually
|
||||
event.preventDefault();
|
||||
openLink(`http://${host}/${location}`);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function booleanFromLocalStorage(key: string, fallback: boolean): boolean {
|
||||
const valueInStorage = localStorage.getItem(key);
|
||||
if (valueInStorage) {
|
||||
return valueInStorage === 'true';
|
||||
} else {
|
||||
localStorage.setItem(key, String(fallback));
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Clamps a value between a min and a max
|
||||
* @param {number} num - Value to clamp
|
||||
* @param {number} min - min value
|
||||
* @param {number} max - max value
|
||||
* @returns {number}
|
||||
*/
|
||||
export const clamp = (num: number, min: number, max: number) =>
|
||||
Math.max(Math.min(num, Math.max(min, max)), Math.min(min, max));
|
||||
@@ -0,0 +1,2 @@
|
||||
export const isOnlyNumbers = /^\d+$/;
|
||||
export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Log, RuntimeStore } from 'ontime-types';
|
||||
|
||||
import { RUNTIME, websocketUrl } from '../api/apiConstants';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
import { addLog } from '../stores/logger';
|
||||
import { runtime } from '../stores/runtime';
|
||||
|
||||
export let websocket: WebSocket | null = null;
|
||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
const reconnectInterval = 1000;
|
||||
let shouldReconnect = true;
|
||||
|
||||
export const connectSocket = () => {
|
||||
websocket = new WebSocket(websocketUrl);
|
||||
|
||||
websocket.onopen = () => {
|
||||
clearTimeout(reconnectTimeout as NodeJS.Timeout);
|
||||
};
|
||||
|
||||
websocket.onclose = () => {
|
||||
console.warn('WebSocket disconnected');
|
||||
if (shouldReconnect) {
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
console.warn('WebSocket: attempting reconnect');
|
||||
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
||||
connectSocket();
|
||||
}
|
||||
}, reconnectInterval);
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
websocket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
const { type, payload } = data;
|
||||
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: implement partial store updates
|
||||
switch (type) {
|
||||
case 'ontime-log': {
|
||||
addLog(payload as Log);
|
||||
break;
|
||||
}
|
||||
case 'ontime': {
|
||||
runtime.setState(payload as RuntimeStore);
|
||||
if (import.meta.env.DEV) {
|
||||
ontimeQueryClient.setQueryData(RUNTIME, data.payload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ontime-playback': {
|
||||
const state = runtime.getState();
|
||||
state.playback = payload;
|
||||
runtime.setState(state);
|
||||
break;
|
||||
}
|
||||
case 'ontime-timer': {
|
||||
const state = runtime.getState();
|
||||
state.timer = payload;
|
||||
runtime.setState(state);
|
||||
break;
|
||||
}
|
||||
case 'ontime-loaded': {
|
||||
const state = runtime.getState();
|
||||
state.loaded = payload;
|
||||
runtime.setState(state);
|
||||
break;
|
||||
}
|
||||
case 'ontime-titles': {
|
||||
const state = runtime.getState();
|
||||
state.titles = payload;
|
||||
runtime.setState(state);
|
||||
break;
|
||||
}
|
||||
case 'ontime-titlesPublic': {
|
||||
const state = runtime.getState();
|
||||
state.titlesPublic = payload;
|
||||
runtime.setState(state);
|
||||
break;
|
||||
}
|
||||
case 'ontime-timerMessage': {
|
||||
const state = runtime.getState();
|
||||
state.timerMessage = payload;
|
||||
runtime.setState(state);
|
||||
break;
|
||||
}
|
||||
case 'ontime-publicMessage': {
|
||||
const state = runtime.getState();
|
||||
state.publicMessage = payload;
|
||||
runtime.setState(state);
|
||||
break;
|
||||
}
|
||||
case 'ontime-lowerMessage': {
|
||||
const state = runtime.getState();
|
||||
state.lowerMessage = payload;
|
||||
runtime.setState(state);
|
||||
break;
|
||||
}
|
||||
case 'ontime-onAir': {
|
||||
const state = runtime.getState();
|
||||
state.onAir = payload;
|
||||
runtime.setState(state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore unhandled
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const disconnectSocket = () => {
|
||||
shouldReconnect = false;
|
||||
websocket?.close();
|
||||
};
|
||||
|
||||
export const socketSend = (message: any) => {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send(message);
|
||||
}
|
||||
};
|
||||
|
||||
export const socketSendJson = (type: string, payload?: any) => {
|
||||
socketSend(
|
||||
JSON.stringify({
|
||||
type,
|
||||
payload,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import Color from 'color';
|
||||
|
||||
type ColourCombination = {
|
||||
backgroundColor: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Selects text colour to maintain accessible contrast
|
||||
* @param bgColour
|
||||
* @return {{backgroundColor, color: string}}
|
||||
*/
|
||||
export const getAccessibleColour = (bgColour: string): ColourCombination => {
|
||||
if (bgColour) {
|
||||
try {
|
||||
const textColor = Color(bgColour).isLight() ? 'black' : '#fffffa';
|
||||
return { backgroundColor: bgColour, color: textColor };
|
||||
} catch (error) {
|
||||
console.log(`Unable to parse colour: ${bgColour}`);
|
||||
}
|
||||
}
|
||||
return { backgroundColor: '#000', color: "#fffffa" };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Creates a list of classnames from array of css module conditions
|
||||
* @param classNames - css modules objects
|
||||
*/
|
||||
export const cx = (classNames: any[]) => classNames.filter(Boolean).join(" ");
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Settings } from 'ontime-types';
|
||||
import { formatFromMillis, millisToString } from 'ontime-utils';
|
||||
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
/**
|
||||
* Returns current time in milliseconds
|
||||
* @returns {number}
|
||||
*/
|
||||
export const nowInMillis = () => {
|
||||
const now = new Date();
|
||||
|
||||
// extract milliseconds since midnight
|
||||
let elapsed = now.getHours() * 3600000;
|
||||
elapsed += now.getMinutes() * 60000;
|
||||
elapsed += now.getSeconds() * 1000;
|
||||
elapsed += now.getMilliseconds();
|
||||
|
||||
return elapsed;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Resolves format from url and store
|
||||
* @return {string|undefined}
|
||||
*/
|
||||
export const resolveTimeFormat = () => {
|
||||
const params = new URL(document.location.href).searchParams;
|
||||
const urlOptions = params.get('format');
|
||||
const settings: Settings | undefined = ontimeQueryClient.getQueryData(APP_SETTINGS);
|
||||
|
||||
return urlOptions || settings?.timeFormat;
|
||||
};
|
||||
|
||||
type FormatOptions = {
|
||||
showSeconds?: boolean;
|
||||
format?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description utility function to format a date in 12 or 24 hour format
|
||||
* @param {number | null} milliseconds
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.showSeconds]
|
||||
* @param {string} [options.format]
|
||||
* @param {function} resolver
|
||||
* @return {string}
|
||||
*/
|
||||
export const formatTime = (milliseconds: number | null, options?: FormatOptions, resolver = resolveTimeFormat) => {
|
||||
if (milliseconds === null) {
|
||||
return '...';
|
||||
}
|
||||
const timeFormat = resolver();
|
||||
const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {};
|
||||
return timeFormat === '12' ? formatFromMillis(milliseconds, formatString) : millisToString(milliseconds, showSeconds);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* millis to seconds
|
||||
* @type {number}
|
||||
*/
|
||||
export const mts = 1000;
|
||||
|
||||
/**
|
||||
* millis to minutes
|
||||
* @type {number}
|
||||
*/
|
||||
export const mtm = 1000 * 60;
|
||||
|
||||
|
||||
/**
|
||||
* millis to hours
|
||||
* @type {number}
|
||||
*/
|
||||
export const mth = 1000 * 60 * 60;
|
||||
|
||||
/**
|
||||
* milliseconds in a day
|
||||
* @type {number}
|
||||
*/
|
||||
export const DAY_TO_MS = 86400000;
|
||||
@@ -0,0 +1,55 @@
|
||||
export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride';
|
||||
|
||||
/**
|
||||
* @description Milliseconds in a day
|
||||
*/
|
||||
export const DAY_TO_MS = 86400000;
|
||||
|
||||
/**
|
||||
* @description calculates duration from given values
|
||||
*/
|
||||
export const calculateDuration = (start: number, end: number): number =>
|
||||
start > end ? end + DAY_TO_MS - start : end - start;
|
||||
|
||||
/**
|
||||
* @description Checks which field the value relates to
|
||||
*/
|
||||
export const handleTimeEntry = (
|
||||
field: TimeEntryField,
|
||||
val: number,
|
||||
timeStart: number,
|
||||
timeEnd: number,
|
||||
): { start: number; end: number; durationOverride: boolean } => {
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
let durationOverride = false;
|
||||
|
||||
if (field === 'timeStart') {
|
||||
start = val;
|
||||
} else if (field === 'timeEnd') {
|
||||
end = val;
|
||||
} else {
|
||||
durationOverride = field === 'durationOverride';
|
||||
}
|
||||
return { start, end, durationOverride };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Validates time entry
|
||||
*/
|
||||
export const validateEntry = (
|
||||
field: TimeEntryField,
|
||||
value: number,
|
||||
timeStart: number,
|
||||
timeEnd: number,
|
||||
): { value: boolean; warnings: { start?: string; end?: string; duration?: string } } => {
|
||||
const validate = { value: true, warnings: { start: '', end: '', duration: '' } };
|
||||
|
||||
const { start, end } = handleTimeEntry(field, value, timeStart, timeEnd);
|
||||
|
||||
if (end < start) {
|
||||
validate.warnings.start = 'Start time later than end time';
|
||||
}
|
||||
|
||||
return validate;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export function isStringBoolean(text: string | null) {
|
||||
if (text === null) {
|
||||
return false;
|
||||
}
|
||||
return text?.toLowerCase() === 'true' || text === '1';
|
||||
}
|
||||
Reference in New Issue
Block a user