mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 03:43:50 +00:00
V3 (#657)
* refactor: cleanup routes * style: smaller base font * chore: upgrade dependencies * chore: lock node version to electron * refactor: pass HTTP to integration controller (#652) * refactor: deprecate onair control * refactor: remove playback router * Several project files user folder (#617) * chore: automated screenshots (#667) * feat: app settings (#658) * refactor: remove deprecated event data (#674) * Studio clock (#663) --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Feat: reorder events with alt+ctrl + arrow up/down (#645) * Warning and danger per event (#677) --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * refactor: stabilise actionHandler (#683) Co-authored-by: Fabian Posenau <fabian@fphome.de> * improvement: hide seconds (#675) * wip: overview (#688) * fix: focus cursor (#695) * refactor: update lower third (#665) * Refactor/time formatting (#696) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * feat: multiple selection (#703) --------- Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Alex <ac@omnivox.dk> * fix: test - go to `Edit mode` befor tying to click `Event options` button (#708) * refactor: runtime service (#715) * fix: issue with loosing cursor position on message (#719) * remove info panel (#721) * Event editor continue (#722) * update API - part (#709) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * refactor: update timers (#729) * feat: many timers (#706) --------- Co-authored-by: arc-alex <ac@omnivox.dk> * refactor: excel cleanup (#734) * refactor: allow import of blocks and skip import (#735) * Project manager (#697) * refactor: UI for linking events (#763) * upgraded pipeline actions (#777) * Over under (#771) * custom fields (#744) --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Sheets settings (#774) --------- Co-authored-by: arc-alex <ac@omnivox.dk> * style: tweaks to lower thirds (#785) * refactor: delays account for gaps (#784) * refactor: partial state updates (#780) * feat: generate crash report (#787) * Sheet use limited input device auth flow (#782) --------- Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Custom fields views (#789) * refactor: deprecate presenter and subtitle (#795) * refactor: organise API around resources (#798) --------- Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com> * Time to end (#804) * Skip fixes (#805) * fix: onair derives from playback * Param nav (#822) --------- Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk> * refactor: download files from interface (#831) * Quick options (#814) * End pause (#832) * chore: bump node version in docker (#834) * refactor: follow in run mode (#840) * fix: uncaught error in http integration (#837) * Apply project (#843) Co-authored-by: Matteo Gheza <matteo.gheza07@gmail.com> Co-authored-by: Ary <arylmoraesn@gmail.com> Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Alex Rohleder <alexrohleder96@gmail.com> Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com> Co-authored-by: Fabian Posenau <fabianpos99+github@gmail.com>
This commit is contained in:
@@ -1,95 +1,4 @@
|
||||
import {
|
||||
forgivingStringToMillis,
|
||||
millisToDelayString,
|
||||
millisToMinutes,
|
||||
millisToSeconds,
|
||||
secondsInMillis,
|
||||
} from '../dateConfig';
|
||||
|
||||
describe('test secondsInMillis function', () => {
|
||||
it('return 0 if value is null', () => {
|
||||
expect(secondsInMillis(null)).toBe(0);
|
||||
});
|
||||
it('returns the seconds value of a millis date', () => {
|
||||
const date = 1686255053619; // Thu Jun 08 2023 20:10:53
|
||||
const seconds = secondsInMillis(date);
|
||||
expect(seconds).toBe(53);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
import { forgivingStringToMillis, millisToDelayString } from '../dateConfig';
|
||||
|
||||
describe('test forgivingStringToMillis()', () => {
|
||||
describe('function handles time with no separators', () => {
|
||||
@@ -105,6 +14,7 @@ describe('test forgivingStringToMillis()', () => {
|
||||
{ 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: '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 },
|
||||
@@ -351,10 +261,10 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
describe('millisToDelayString()', () => {
|
||||
it('returns null for null values', () => {
|
||||
expect(millisToDelayString(null)).toBeNull();
|
||||
expect(millisToDelayString(null)).toBe('');
|
||||
});
|
||||
it('returns null 0', () => {
|
||||
expect(millisToDelayString(0)).toBeNull();
|
||||
expect(millisToDelayString(0)).toBe('');
|
||||
});
|
||||
describe('converts values in seconds', () => {
|
||||
it('shows a simple string with value in seconds', () => {
|
||||
@@ -370,7 +280,6 @@ describe('millisToDelayString()', () => {
|
||||
expect(millisToDelayString(value)?.endsWith('sec')).toBe(true);
|
||||
});
|
||||
});
|
||||
expect(millisToDelayString(null)).toBeNull();
|
||||
});
|
||||
|
||||
describe('converts values in minutes', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EndAction, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
||||
import { EndAction, EventCustomFields, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
||||
|
||||
import { cloneEvent } from '../eventsManager';
|
||||
|
||||
@@ -9,8 +9,6 @@ describe('cloneEvent()', () => {
|
||||
type: SupportedEvent.Event,
|
||||
title: 'title',
|
||||
cue: 'cue',
|
||||
subtitle: 'subtitle',
|
||||
presenter: 'presenter',
|
||||
note: 'note',
|
||||
timeStart: 0,
|
||||
duration: 10,
|
||||
@@ -21,16 +19,11 @@ describe('cloneEvent()', () => {
|
||||
skip: false,
|
||||
colour: 'F00',
|
||||
revision: 10,
|
||||
user0: 'user0',
|
||||
user1: 'user1',
|
||||
user2: 'user2',
|
||||
user3: 'user3',
|
||||
user4: 'user4',
|
||||
user5: 'user5',
|
||||
user6: 'user6',
|
||||
user7: 'user7',
|
||||
user8: 'user8',
|
||||
user9: 'user9',
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
custom: {
|
||||
lighting: { value: '3' },
|
||||
} as EventCustomFields,
|
||||
} as OntimeEvent;
|
||||
|
||||
const cloned = cloneEvent(original);
|
||||
@@ -38,8 +31,6 @@ describe('cloneEvent()', () => {
|
||||
// @ts-expect-error -- safeguarding this
|
||||
expect(cloned?.id).toBe(undefined);
|
||||
expect(cloned.title).toBe(original.title);
|
||||
expect(cloned.subtitle).toBe(original.subtitle);
|
||||
expect(cloned.presenter).toBe(original.presenter);
|
||||
expect(cloned.note).toBe(original.note);
|
||||
expect(cloned.endAction).toBe(original.endAction);
|
||||
expect(cloned.timerType).toBe(original.timerType);
|
||||
@@ -51,5 +42,8 @@ describe('cloneEvent()', () => {
|
||||
expect(cloned.colour).toBe(original.colour);
|
||||
expect(cloned.type).toBe(SupportedEvent.Event);
|
||||
expect(cloned.revision).toBe(0);
|
||||
expect(cloned.timeWarning).toBe(original.timeWarning);
|
||||
expect(cloned.timeDanger).toBe(original.timeDanger);
|
||||
expect(cloned.custom).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isIPAddress, isOnlyNumbers, startsWithHttp } from '../regex';
|
||||
import { isAlphanumeric, isIPAddress, isNotEmpty, isOnlyNumbers, startsWithHttp, startsWithSlash } from '../regex';
|
||||
|
||||
describe('simple tests for regex', () => {
|
||||
test('isOnlyNumbers', () => {
|
||||
@@ -36,4 +36,40 @@ describe('simple tests for regex', () => {
|
||||
expect(startsWithHttp.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('startsWithSlash', () => {
|
||||
const right = ['//test'];
|
||||
const wrong = ['testing', '123.0.1'];
|
||||
|
||||
right.forEach((t) => {
|
||||
expect(startsWithSlash.test(t)).toBe(true);
|
||||
});
|
||||
wrong.forEach((t) => {
|
||||
expect(startsWithSlash.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('isAlphanumeric', () => {
|
||||
const right = ['dsafdsafa9f9sdafdsSADFHASDF', '1231', '1', 'a', 'asdas1asdas', '11as', '1'];
|
||||
const wrong = ['with space', 'with @', '#'];
|
||||
|
||||
right.forEach((t) => {
|
||||
expect(isAlphanumeric.test(t)).toBe(true);
|
||||
});
|
||||
wrong.forEach((t) => {
|
||||
expect(isAlphanumeric.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('isNotEmpty', () => {
|
||||
const right = ['notempty'];
|
||||
const wrong = ['', ' '];
|
||||
|
||||
right.forEach((t) => {
|
||||
expect(isNotEmpty.test(t)).toBe(true);
|
||||
});
|
||||
wrong.forEach((t) => {
|
||||
expect(isNotEmpty.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import { formatTime } from '../time';
|
||||
import { formatTime, nowInMillis } from '../time';
|
||||
|
||||
describe('nowInMillis()', () => {
|
||||
it('should return the current time in milliseconds', () => {
|
||||
const mockDate = new Date(2022, 1, 1, 13, 0, 0); // This date corresponds to 13:00:00
|
||||
const expectedMillis = 13 * 60 * 60 * 1000;
|
||||
const dateSpy = vi.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
|
||||
|
||||
const result = nowInMillis();
|
||||
|
||||
expect(result).toBe(expectedMillis);
|
||||
dateSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatTime()', () => {
|
||||
it('parses 24h strings', () => {
|
||||
const ms = 13 * 60 * 60 * 1000;
|
||||
const options = {
|
||||
showSeconds: true,
|
||||
format: 'irrelevant',
|
||||
};
|
||||
const time = formatTime(ms, options, () => '24');
|
||||
const time = formatTime(ms, { format12: 'hh:mm:ss', format24: 'HH:mm:ss' }, (_format12, format24) => format24);
|
||||
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');
|
||||
const time = formatTime(ms, { format12: 'hh:mm:ss a', format24: 'HH:mm:ss' }, (format12, _format24) => format12);
|
||||
expect(time).toStrictEqual('01:00:00 PM');
|
||||
});
|
||||
|
||||
@@ -27,13 +32,9 @@ describe('formatTime()', () => {
|
||||
expect(time).toStrictEqual('...');
|
||||
});
|
||||
|
||||
it('shows 12h format without times', () => {
|
||||
const ms = 13 * 60 * 60 * 1000;
|
||||
const options = {
|
||||
showSeconds: false,
|
||||
format: 'hh:mm a',
|
||||
};
|
||||
const time = formatTime(ms, options, () => '12');
|
||||
expect(time).toStrictEqual('01:00 PM');
|
||||
it('handles negative times', () => {
|
||||
const ms = 1 * 60 * 60 * 1000;
|
||||
const time = formatTime(-ms, { format12: 'hh:mm a', format24: 'HH:mm' }, (_format12, format24) => format24);
|
||||
expect(time).toStrictEqual('-01:00');
|
||||
});
|
||||
});
|
||||
|
||||
+17
-17
@@ -1,8 +1,8 @@
|
||||
import { resolvePath } from 'react-router-dom';
|
||||
|
||||
import { generateURLFromAlias, getAliasRoute, validateAlias } from '../aliases';
|
||||
import { generateUrlFromPreset, getRouteFromPreset, validateUrlPresetPath } from '../urlPresets';
|
||||
|
||||
describe('An alias fails if incorrect', () => {
|
||||
describe('A preset fails if incorrect', () => {
|
||||
const testsToFail = [
|
||||
// no empty
|
||||
'',
|
||||
@@ -21,11 +21,11 @@ describe('An alias fails if incorrect', () => {
|
||||
|
||||
testsToFail.forEach((t) =>
|
||||
it(`${t}`, () => {
|
||||
expect(validateAlias(t).status).toBeFalsy();
|
||||
expect(validateUrlPresetPath(t).isValid).toBeFalsy();
|
||||
}),
|
||||
);
|
||||
});
|
||||
describe('generateURLFromAlias and getAliasRoute function', () => {
|
||||
describe('generateUrlFromPreset and getRouteFromPreset function', () => {
|
||||
test('generate the expected url from an alias', () => {
|
||||
const testData = [
|
||||
{
|
||||
@@ -41,10 +41,10 @@ describe('generateURLFromAlias and getAliasRoute function', () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(generateURLFromAlias(testData[0])).toStrictEqual(expected[0].url);
|
||||
expect(generateUrlFromPreset(testData[0])).toStrictEqual(expected[0].url);
|
||||
});
|
||||
test('generate the url to redirect to when the current URL is just the alias', () => {
|
||||
const aliases = [
|
||||
const presets = [
|
||||
{
|
||||
enabled: true,
|
||||
alias: 'demopage',
|
||||
@@ -52,7 +52,7 @@ describe('generateURLFromAlias and getAliasRoute function', () => {
|
||||
},
|
||||
];
|
||||
// let current location be the alias
|
||||
const location = resolvePath(aliases[0].alias);
|
||||
const location = resolvePath(presets[0].alias);
|
||||
|
||||
const expected = [
|
||||
{
|
||||
@@ -60,10 +60,10 @@ describe('generateURLFromAlias and getAliasRoute function', () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(getAliasRoute(location, aliases, null)).toStrictEqual(expected[0].url);
|
||||
expect(getRouteFromPreset(location, presets, null)).toStrictEqual(expected[0].url);
|
||||
});
|
||||
test('generate the url to redirect to when the current URL the same url but with a change of params', () => {
|
||||
const aliases = [
|
||||
const presets = [
|
||||
{
|
||||
enabled: true,
|
||||
alias: 'demopage',
|
||||
@@ -71,22 +71,22 @@ describe('generateURLFromAlias and getAliasRoute function', () => {
|
||||
},
|
||||
];
|
||||
// let current location be the actual url with alias attached to it
|
||||
const location = resolvePath(aliases[0].pathAndParams);
|
||||
const location = resolvePath(presets[0].pathAndParams);
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
urlSearchParams.append('alias', aliases[0].alias); //
|
||||
urlSearchParams.append('alias', presets[0].alias); //
|
||||
|
||||
// update current alias with extra param
|
||||
aliases[0].pathAndParams += '&eventId=674';
|
||||
presets[0].pathAndParams += '&eventId=674';
|
||||
const expected = [
|
||||
{
|
||||
url: '/timer?user=guest&eventId=674&alias=demopage',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getAliasRoute(location, aliases, urlSearchParams)).toStrictEqual(expected[0].url);
|
||||
expect(getRouteFromPreset(location, presets, urlSearchParams)).toStrictEqual(expected[0].url);
|
||||
});
|
||||
test('generate no url to redirect to when the current URL the same url', () => {
|
||||
const aliases = [
|
||||
const presets = [
|
||||
{
|
||||
enabled: true,
|
||||
alias: 'demopage',
|
||||
@@ -94,10 +94,10 @@ describe('generateURLFromAlias and getAliasRoute function', () => {
|
||||
},
|
||||
];
|
||||
// let current location be the actual url with alias attached to it
|
||||
const location = resolvePath(aliases[0].pathAndParams);
|
||||
const location = resolvePath(presets[0].pathAndParams);
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
urlSearchParams.append('alias', aliases[0].alias); //
|
||||
urlSearchParams.append('alias', presets[0].alias); //
|
||||
|
||||
expect(getAliasRoute(location, aliases, urlSearchParams)).toBeNull();
|
||||
expect(getRouteFromPreset(location, presets, urlSearchParams)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { Location, resolvePath } from 'react-router-dom';
|
||||
import { Alias } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the URL to send an alias to
|
||||
* @param location
|
||||
* @param data
|
||||
* @param searchParams
|
||||
*/
|
||||
export const getAliasRoute = (location: Location, data: Alias[], searchParams: URLSearchParams) => {
|
||||
const currentURL = location.pathname.substring(1);
|
||||
// we need to check if the whole url here is an alias, so we can redirect
|
||||
const foundAlias = data.filter((d) => d.alias === currentURL && d.enabled)[0];
|
||||
if (foundAlias) {
|
||||
return generateURLFromAlias(foundAlias);
|
||||
}
|
||||
const aliasOnPage = searchParams.get('alias');
|
||||
for (const d of data) {
|
||||
if (aliasOnPage) {
|
||||
// if the alias fits the alias on this page, but the URL is different, we redirect user to the new URL
|
||||
// if we have the same alias and its enabled and its not empty
|
||||
if (d.alias !== '' && d.enabled && d.alias === aliasOnPage) {
|
||||
const newAliasPath = resolvePath(d.pathAndParams);
|
||||
const urlParams = new URLSearchParams(newAliasPath.search);
|
||||
urlParams.set('alias', d.alias);
|
||||
// we confirm either the url parameters does not match or the url path doesnt
|
||||
if (!isEqual(urlParams, searchParams) || newAliasPath.pathname !== location.pathname) {
|
||||
// we then redirect to the alias route, since the view listening to this alias has an outdated URL
|
||||
return `${newAliasPath.pathname}?${urlParams}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate URL from an alias
|
||||
* @param aliasData
|
||||
*/
|
||||
export const generateURLFromAlias = (aliasData: Alias) => {
|
||||
const newAliasPath = resolvePath(aliasData.pathAndParams);
|
||||
const urlParams = new URLSearchParams(newAliasPath.search);
|
||||
urlParams.set('alias', aliasData.alias);
|
||||
|
||||
return `${newAliasPath.pathname}?${urlParams}`;
|
||||
};
|
||||
@@ -1,37 +1,5 @@
|
||||
import { formatFromMillis } from 'ontime-utils';
|
||||
|
||||
import { mth, mtm, mts } from './timeConstants';
|
||||
|
||||
export const timeFormat = 'HH:mm';
|
||||
export const timeFormatSeconds = 'HH:mm:ss';
|
||||
|
||||
export function secondsInMillis(millis: number | null) {
|
||||
if (!millis) {
|
||||
return 0;
|
||||
}
|
||||
return Math.floor((millis % mtm) / mts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
};
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
import { formatFromMillis, MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* @description safe parse string to int
|
||||
@@ -78,17 +46,19 @@ function checkAmPm(value: string) {
|
||||
* @param {string} value
|
||||
*/
|
||||
function checkMatchers(value: string) {
|
||||
const hoursMatch = /(\d+)h/.exec(value);
|
||||
const hoursMatch = /(\d+)h/i.exec(value);
|
||||
const hoursMatchValue = hoursMatch ? parse(hoursMatch[1]) : 0;
|
||||
|
||||
const minutesMatch = /(\d+)m/.exec(value);
|
||||
const minutesMatch = /(\d+)m/i.exec(value);
|
||||
const minutesMatchValue = minutesMatch ? parse(minutesMatch[1]) : 0;
|
||||
|
||||
const secondsMatch = /(\d+)s/.exec(value);
|
||||
const secondsMatch = /(\d+)s/i.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 * MILLIS_PER_HOUR + minutesMatchValue * MILLIS_PER_MINUTE + secondsMatchValue * MILLIS_PER_SECOND
|
||||
);
|
||||
}
|
||||
return { hoursMatchValue };
|
||||
}
|
||||
@@ -105,13 +75,13 @@ function inferSeparators(value: string, isAM: boolean, isPM: boolean) {
|
||||
let addAM = 0;
|
||||
if (length === 1) {
|
||||
if (isPM || isAM) {
|
||||
inferredMillis = parse(value) * mth;
|
||||
inferredMillis = parse(value) * MILLIS_PER_HOUR;
|
||||
if (isAM) {
|
||||
// this ensures we dont add 12 hours in the end
|
||||
addAM = inferredMillis;
|
||||
}
|
||||
} else {
|
||||
inferredMillis = parse(value) * mtm;
|
||||
inferredMillis = parse(value) * MILLIS_PER_MINUTE;
|
||||
}
|
||||
} else if (length === 2) {
|
||||
if (isPM || isAM) {
|
||||
@@ -121,22 +91,22 @@ function inferSeparators(value: string, isAM: boolean, isPM: boolean) {
|
||||
addAM = 12;
|
||||
}
|
||||
} else {
|
||||
inferredMillis = parse(value) * mtm;
|
||||
inferredMillis = parse(value) * MILLIS_PER_MINUTE;
|
||||
}
|
||||
} else if (length === 3) {
|
||||
inferredMillis = parse(value[0]) * mth + parse(value.substring(1)) * mtm;
|
||||
inferredMillis = parse(value[0]) * MILLIS_PER_HOUR + parse(value.substring(1)) * MILLIS_PER_MINUTE;
|
||||
} else if (length === 4) {
|
||||
inferredMillis = parse(value.substring(0, 2)) * mth + parse(value.substring(2)) * mtm;
|
||||
inferredMillis = parse(value.substring(0, 2)) * MILLIS_PER_HOUR + parse(value.substring(2)) * MILLIS_PER_MINUTE;
|
||||
} 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;
|
||||
inferredMillis = hours * MILLIS_PER_HOUR + minutes * MILLIS_PER_MINUTE + seconds * MILLIS_PER_SECOND;
|
||||
} 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;
|
||||
inferredMillis = hours * MILLIS_PER_HOUR + minutes * MILLIS_PER_MINUTE + seconds * MILLIS_PER_SECOND;
|
||||
}
|
||||
return { inferredMillis, addAM };
|
||||
}
|
||||
@@ -167,9 +137,9 @@ export const forgivingStringToMillis = (value: string): number => {
|
||||
|
||||
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;
|
||||
millis = parse(first) * MILLIS_PER_HOUR;
|
||||
millis += parse(second) * MILLIS_PER_MINUTE;
|
||||
millis += parse(third) * MILLIS_PER_SECOND;
|
||||
} else if (first != null && second == null && third == null) {
|
||||
// we only have one section, infer separators
|
||||
const { inferredMillis, addAM } = inferSeparators(first, isAM, isPM);
|
||||
@@ -177,30 +147,33 @@ export const forgivingStringToMillis = (value: string): number => {
|
||||
hoursMatchValue = addAM;
|
||||
}
|
||||
if (first != null && second != null && third == null) {
|
||||
millis = parse(first) * mth;
|
||||
millis += parse(second) * mtm;
|
||||
millis = parse(first) * MILLIS_PER_HOUR;
|
||||
millis += parse(second) * MILLIS_PER_MINUTE;
|
||||
}
|
||||
|
||||
// Add 12 hours if it is PM
|
||||
if (isPM && hoursMatchValue < 12) {
|
||||
millis += 12 * mth;
|
||||
millis += 12 * MILLIS_PER_HOUR;
|
||||
}
|
||||
return millis;
|
||||
};
|
||||
|
||||
export function millisToDelayString(millis: number | null): undefined | string | null {
|
||||
export function millisToDelayString(millis: MaybeNumber, format: 'compact' | 'expanded' = 'compact'): string {
|
||||
if (millis == null || millis === 0) {
|
||||
return null;
|
||||
return '';
|
||||
}
|
||||
|
||||
const isNegative = millis < 0;
|
||||
const absMillis = Math.abs(millis);
|
||||
const isCompact = format === 'compact';
|
||||
const delayed = isCompact ? '+' : 'delayed by ';
|
||||
const ahead = isCompact ? '-' : 'ahead by ';
|
||||
|
||||
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')}`;
|
||||
if (absMillis < MILLIS_PER_MINUTE) {
|
||||
return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 's')} sec`;
|
||||
} else if (absMillis < MILLIS_PER_HOUR && absMillis % MILLIS_PER_MINUTE === 0) {
|
||||
return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 'm')} min`;
|
||||
}
|
||||
|
||||
return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 'HH:mm:ss')}`;
|
||||
}
|
||||
|
||||
@@ -6,27 +6,26 @@ import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
* @param {string} [after]
|
||||
* @return {OntimeEvent} clean event
|
||||
*/
|
||||
type ClonedEvent = Omit<
|
||||
OntimeEvent,
|
||||
'id' | 'user0' | 'user1' | 'user2' | 'user3' | 'user4' | 'user5' | 'user6' | 'user7' | 'user8' | 'user9'
|
||||
>;
|
||||
type ClonedEvent = Omit<OntimeEvent, 'id' | 'cue'>;
|
||||
export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
||||
return {
|
||||
type: SupportedEvent.Event,
|
||||
title: event.title,
|
||||
cue: event.cue,
|
||||
subtitle: event.subtitle,
|
||||
presenter: event.presenter,
|
||||
note: event.note,
|
||||
timeStart: event.timeStart,
|
||||
duration: event.duration,
|
||||
timeEnd: event.timeEnd,
|
||||
timerType: event.timerType,
|
||||
timeStrategy: event.timeStrategy,
|
||||
linkStart: event.linkStart,
|
||||
endAction: event.endAction,
|
||||
isPublic: event.isPublic,
|
||||
skip: event.skip,
|
||||
colour: event.colour,
|
||||
after: after,
|
||||
after,
|
||||
revision: 0,
|
||||
timeWarning: event.timeWarning,
|
||||
timeDanger: event.timeDanger,
|
||||
custom: {},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
|
||||
|
||||
type FileOptions = {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type BlobOptions = {
|
||||
type: string;
|
||||
};
|
||||
|
||||
export default async function fileDownload(url: string, fileOptions: FileOptions, blobOptions: BlobOptions) {
|
||||
const response = await axios({
|
||||
url: `${url}/db`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
const headerLine = response.headers['Content-Disposition'];
|
||||
let { name: fileName } = fileOptions;
|
||||
const { type: fileType } = fileOptions;
|
||||
const { project, rundown, userFields } = response.data;
|
||||
|
||||
// try and get the filename from the response
|
||||
if (headerLine != null) {
|
||||
const startFileNameIndex = headerLine.indexOf('"') + 1;
|
||||
const endFileNameIndex = headerLine.lastIndexOf('"');
|
||||
fileName = headerLine.substring(startFileNameIndex, endFileNameIndex);
|
||||
}
|
||||
|
||||
let fileContent = '';
|
||||
|
||||
if (fileType === 'json') {
|
||||
fileContent = JSON.stringify(response.data);
|
||||
fileName += '.json';
|
||||
}
|
||||
|
||||
if (fileType === 'csv') {
|
||||
const sheetData = makeTable(project, rundown, userFields);
|
||||
fileContent = makeCSV(sheetData);
|
||||
fileName += '.csv';
|
||||
}
|
||||
|
||||
const blob = new Blob([fileContent], { type: blobOptions.type });
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', downloadUrl);
|
||||
link.setAttribute('download', fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
// Clean up the URL.createObjectURL to release resources
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
return;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { KeyboardEvent } from 'react';
|
||||
|
||||
export function isKeyEnter<T>(event: KeyboardEvent<T>): boolean {
|
||||
return event.key === 'Enter';
|
||||
}
|
||||
|
||||
export function isKeyEscape<T>(event: KeyboardEvent<T>): boolean {
|
||||
return event.key === 'Escape';
|
||||
}
|
||||
@@ -1,3 +1,12 @@
|
||||
/**
|
||||
* Simple regex patterns for common use cases
|
||||
* mostly used in form validation
|
||||
*/
|
||||
|
||||
export const isOnlyNumbers = /^\d+$/;
|
||||
export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||
export const startsWithHttp = /^http:\/\//;
|
||||
export const startsWithSlash = /^\//;
|
||||
export const isAlphanumeric = /^[a-z0-9]+$/i;
|
||||
export const isASCII = /^[ -~]+$/; //https://catonmat.net/my-favorite-regex
|
||||
export const isNotEmpty = /\S/;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Log, RuntimeStore } from 'ontime-types';
|
||||
|
||||
import { isProduction, RUNTIME, websocketUrl } from '../api/apiConstants';
|
||||
import { isProduction, RUNTIME, websocketUrl } from '../api/constants';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
import { socketClientName } from '../stores/connectionName';
|
||||
import { addLog } from '../stores/logger';
|
||||
import { runtime } from '../stores/runtime';
|
||||
import { patchRuntime, runtimeStore } from '../stores/runtime';
|
||||
|
||||
export let websocket: WebSocket | null = null;
|
||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
@@ -12,6 +12,7 @@ const reconnectInterval = 1000;
|
||||
export let shouldReconnect = true;
|
||||
export let hasConnected = false;
|
||||
export let reconnectAttempts = 0;
|
||||
|
||||
export const connectSocket = (preferredClientName?: string) => {
|
||||
websocket = new WebSocket(websocketUrl);
|
||||
|
||||
@@ -52,7 +53,6 @@ export const connectSocket = (preferredClientName?: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: implement partial store updates
|
||||
switch (type) {
|
||||
case 'client-name': {
|
||||
socketClientName.getState().setName(payload);
|
||||
@@ -63,52 +63,60 @@ export const connectSocket = (preferredClientName?: string) => {
|
||||
break;
|
||||
}
|
||||
case 'ontime': {
|
||||
runtime.setState(payload as RuntimeStore);
|
||||
runtimeStore.setState(payload as RuntimeStore);
|
||||
if (!isProduction) {
|
||||
ontimeQueryClient.setQueryData(RUNTIME, data.payload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ontime-playback': {
|
||||
const state = runtime.getState();
|
||||
state.playback = payload;
|
||||
runtime.setState(state);
|
||||
case 'ontime-clock': {
|
||||
patchRuntime('clock', payload);
|
||||
updateDevTools({ clock: payload });
|
||||
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-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);
|
||||
patchRuntime('timer', payload);
|
||||
updateDevTools({ timer: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-onAir': {
|
||||
const state = runtime.getState();
|
||||
state.onAir = payload;
|
||||
runtime.setState(state);
|
||||
patchRuntime('onAir', payload);
|
||||
updateDevTools({ onAir: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-message': {
|
||||
patchRuntime('message', payload);
|
||||
updateDevTools({ message: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-runtime': {
|
||||
patchRuntime('runtime', payload);
|
||||
updateDevTools({ runtime: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-eventNow': {
|
||||
patchRuntime('eventNow', payload);
|
||||
updateDevTools({ eventNow: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-publicEventNow': {
|
||||
patchRuntime('publicEventNow', payload);
|
||||
updateDevTools({ publicEventNow: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-eventNext': {
|
||||
patchRuntime('eventNext', payload);
|
||||
updateDevTools({ eventNext: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-publicEventNext': {
|
||||
patchRuntime('publicEventNext', payload);
|
||||
updateDevTools({ publicEventNext: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-auxtimer1': {
|
||||
patchRuntime('auxtimer1', payload);
|
||||
updateDevTools({ auxtimer1: payload });
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -137,3 +145,12 @@ export const socketSendJson = (type: string, payload?: unknown) => {
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
function updateDevTools(newData: Partial<RuntimeStore>) {
|
||||
if (!isProduction) {
|
||||
ontimeQueryClient.setQueryData(RUNTIME, (oldData: RuntimeStore) => ({
|
||||
...oldData,
|
||||
...newData,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,3 +29,8 @@ export const getAccessibleColour = (bgColour?: string): ColourCombination => {
|
||||
* @param classNames - css modules objects
|
||||
*/
|
||||
export const cx = (classNames: any[]) => classNames.filter(Boolean).join(' ');
|
||||
|
||||
export const enDash = '–';
|
||||
|
||||
export const timerPlaceholder = '––:––:––';
|
||||
export const timerPlaceholderMin = '––:––';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Settings } from 'ontime-types';
|
||||
import { formatFromMillis, millisToString } from 'ontime-utils';
|
||||
import { MaybeNumber, Settings, TimeFormat } from 'ontime-types';
|
||||
import { formatFromMillis } from 'ontime-utils';
|
||||
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
||||
import { APP_SETTINGS } from '../api/constants';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
/**
|
||||
@@ -22,36 +23,75 @@ export const nowInMillis = () => {
|
||||
|
||||
/**
|
||||
* @description Resolves format from url and store
|
||||
* @return {string|undefined}
|
||||
* @return {string|null} A format string like "hh:mm:ss a" or null
|
||||
*/
|
||||
export const resolveTimeFormat = () => {
|
||||
function getFormatFromParams() {
|
||||
const params = new URL(document.location.href).searchParams;
|
||||
const urlOptions = params.get('format');
|
||||
const settings: Settings | undefined = ontimeQueryClient.getQueryData(APP_SETTINGS);
|
||||
return params.get('timeformat');
|
||||
}
|
||||
|
||||
return urlOptions || settings?.timeFormat;
|
||||
};
|
||||
/**
|
||||
* Gets the format options from the applicaton settings
|
||||
* @returns a string equivalent to the format, ie: hh:mm:ss a or HH:mm:ss
|
||||
*/
|
||||
export function getFormatFromSettings(): TimeFormat {
|
||||
const settings: Settings | undefined = ontimeQueryClient.getQueryData(APP_SETTINGS);
|
||||
return settings?.timeFormat ?? '24';
|
||||
}
|
||||
|
||||
export function getDefaultFormat(
|
||||
currentSettings?: TimeFormat,
|
||||
format12: string = FORMAT_12,
|
||||
format24: string = FORMAT_24,
|
||||
): string {
|
||||
if (currentSettings === '12') {
|
||||
return format12;
|
||||
}
|
||||
return format24;
|
||||
}
|
||||
|
||||
function resolveTimeFormat(fallback12: string, fallback24: string): string {
|
||||
// if the user has an option, we use that
|
||||
const formatFromParams = getFormatFromParams();
|
||||
if (formatFromParams) {
|
||||
return formatFromParams;
|
||||
}
|
||||
|
||||
// otherwise we use the view defined, with respect to the 12-24 hour settings
|
||||
const formatFromSettings = getFormatFromSettings();
|
||||
if (formatFromSettings === '12') {
|
||||
return fallback12;
|
||||
}
|
||||
|
||||
return fallback24;
|
||||
}
|
||||
|
||||
type FormatOptions = {
|
||||
showSeconds?: boolean;
|
||||
format?: string;
|
||||
format12: string;
|
||||
format24: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description utility function to format a date in 12 or 24 hour format
|
||||
* @param {number | null} milliseconds
|
||||
* @description viewer specific utility function to format a date in 12 or 24 hour format
|
||||
* @param {MaybeNumber} milliseconds
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.showSeconds]
|
||||
* @param {string} [options.format]
|
||||
* @param {function} resolver
|
||||
* @param {string} [options.format.format12] format string if 12 hour time
|
||||
* @param {string} [options.format.format24] format string if 24 hour time
|
||||
* @param {Function} resolver DI for testing
|
||||
* @return {string}
|
||||
*/
|
||||
export const formatTime = (milliseconds: number | null, options?: FormatOptions, resolver = resolveTimeFormat) => {
|
||||
export const formatTime = (
|
||||
milliseconds: MaybeNumber,
|
||||
options?: FormatOptions,
|
||||
resolver = resolveTimeFormat,
|
||||
): string => {
|
||||
if (milliseconds === null) {
|
||||
return '...';
|
||||
}
|
||||
const timeFormat = resolver();
|
||||
const fallback = options?.showSeconds ? 'hh:mm:ss a' : 'hh:mm a';
|
||||
const { showSeconds = false, format: formatString = fallback } = options || {};
|
||||
return timeFormat === '12' ? formatFromMillis(milliseconds, formatString) : millisToString(milliseconds, showSeconds);
|
||||
|
||||
const timeFormat = resolver(options?.format12 ?? FORMAT_12, options?.format24 ?? FORMAT_24);
|
||||
const display = formatFromMillis(Math.abs(milliseconds), timeFormat);
|
||||
|
||||
const isNegative = milliseconds < 0;
|
||||
return `${isNegative ? '-' : ''}${display}`;
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -1 +0,0 @@
|
||||
export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride';
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Collection of rules for pre-validating a spreadsheet
|
||||
* @param file
|
||||
*/
|
||||
export function validateExcelImport(file: File) {
|
||||
if (!isExcelFile(file)) {
|
||||
throw new Error('Unknown file type');
|
||||
}
|
||||
|
||||
// Check if file is empty
|
||||
if (file.size === 0) {
|
||||
throw new Error('File is empty');
|
||||
}
|
||||
|
||||
// Limit file size of an Excel file to around 10MB
|
||||
if (file.size > 10_000_000) {
|
||||
throw new Error('File size limit (10MB) exceeded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of rules for pre-validating a project file
|
||||
* @param file
|
||||
*/
|
||||
export function validateProjectFile(file: File) {
|
||||
if (!isOntimeFile(file)) {
|
||||
throw new Error('Unknown file type');
|
||||
}
|
||||
|
||||
// Check if file is empty
|
||||
if (file.size === 0) {
|
||||
throw new Error('File is empty');
|
||||
}
|
||||
|
||||
// Limit file size of a project file to around 1MB
|
||||
if (file.size > 1_000_000) {
|
||||
throw new Error('File size limit (10MB) exceeded');
|
||||
}
|
||||
}
|
||||
|
||||
export function isExcelFile(file: File | null) {
|
||||
return file?.name.endsWith('.xlsx');
|
||||
}
|
||||
|
||||
export function isOntimeFile(file: File | null) {
|
||||
return file?.name.endsWith('.json');
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { Location, resolvePath } from 'react-router-dom';
|
||||
import { URLPreset } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Validates a preset against defined parameters
|
||||
* @param {string} preset
|
||||
* @returns {{message: string, isValid: boolean}}
|
||||
*/
|
||||
export const validateUrlPresetPath = (preset: string): { message: string; isValid: boolean } => {
|
||||
if (preset === '' || preset == null) {
|
||||
return { isValid: false, message: 'Path cannot be empty' };
|
||||
}
|
||||
|
||||
if (preset.includes('http') || preset.includes('https') || preset.includes('www')) {
|
||||
return { isValid: false, message: 'Path should not include http, https, www' };
|
||||
}
|
||||
|
||||
if (preset.includes('127.0.0.1') || preset.includes('localhost') || preset.includes('0.0.0.0')) {
|
||||
return { isValid: false, message: 'Path should not include hostname' };
|
||||
}
|
||||
|
||||
if (preset.includes('editor')) {
|
||||
// no editor
|
||||
return { isValid: false, message: 'No path to editor page allowed' };
|
||||
}
|
||||
|
||||
return { isValid: true, message: 'ok' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the URL to send a preset to
|
||||
* @param location
|
||||
* @param data
|
||||
* @param searchParams
|
||||
*/
|
||||
export const getRouteFromPreset = (location: Location, data: URLPreset[], searchParams: URLSearchParams) => {
|
||||
const currentURL = location.pathname.substring(1);
|
||||
|
||||
// we need to check if the whole url here is an alias, so we can redirect
|
||||
const foundPreset = data.filter((d) => d.alias === currentURL && d.enabled)[0];
|
||||
if (foundPreset) {
|
||||
return generateUrlFromPreset(foundPreset);
|
||||
}
|
||||
|
||||
const presetOnPage = searchParams.get('alias');
|
||||
for (const d of data) {
|
||||
if (presetOnPage) {
|
||||
// if the alias fits the preset on this page, but the URL is different, we redirect user to the new URL
|
||||
// if we have the same alias and its enabled and its not empty
|
||||
if (d.alias !== '' && d.enabled && d.alias === presetOnPage) {
|
||||
const newPath = resolvePath(d.pathAndParams);
|
||||
const urlParams = new URLSearchParams(newPath.search);
|
||||
urlParams.set('alias', d.alias);
|
||||
// we confirm either the url parameters does not match or the url path doesnt
|
||||
if (!isEqual(urlParams, searchParams) || newPath.pathname !== location.pathname) {
|
||||
// we then redirect to the alias route, since the view listening to this alias has an outdated URL
|
||||
return `${newPath.pathname}?${urlParams}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate URL from an preset
|
||||
* @param presetData
|
||||
*/
|
||||
export const generateUrlFromPreset = (presetData: URLPreset) => {
|
||||
const newPresetPath = resolvePath(presetData.pathAndParams);
|
||||
const urlParams = new URLSearchParams(newPresetPath.search);
|
||||
urlParams.set('alias', presetData.alias);
|
||||
|
||||
return `${newPresetPath.pathname}?${urlParams}`;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export function isStringBoolean(text: string | null) {
|
||||
if (text === null) {
|
||||
return false;
|
||||
}
|
||||
return text?.toLowerCase() === 'true' || text === '1';
|
||||
}
|
||||
Reference in New Issue
Block a user