Compare commits

...

10 Commits

Author SHA1 Message Date
Carlos Valente 8bc3f5a56c Fix links (#1366)
* chore: remove unused code

* bump version to 3.9.3

* chore: use constant links

* refactor: build links

* chore: ignore ontime-data
2024-12-06 16:48:35 +01:00
Carlos Valente acf1afb5e9 fix: prevent overflow 2024-12-06 14:54:41 +01:00
Carlos Valente d8e2a8d092 bump version to 3.9.2 2024-12-04 21:41:49 +01:00
Carlos Valente 8e587128a1 refactor: ensure element height 2024-12-04 21:41:49 +01:00
Alex Christoffer Rasmussen 607bc40673 create addTime function in simple timer (#1363)
* create addTime function in simple timer
2024-12-04 20:25:49 +01:00
Carlos Valente e6aa7404f7 chore: correct documentation 2024-12-04 19:49:49 +01:00
Carlos Valente d622a7738f refactor: remove deprecated field 2024-12-04 19:49:49 +01:00
Carlos Valente b9ba416366 refactor: add prefix to relative assets
refactor: add prefix to server entrypoints
refactor: add prefix to express router
refactor: add prefix to router basename
2024-12-04 19:49:49 +01:00
Carlos Valente b3ce247f54 bump version to 3.9.1 2024-12-02 14:45:57 +01:00
Carlos Valente 82a2660fbc fix: router prefix 2024-12-02 14:45:57 +01:00
32 changed files with 257 additions and 173 deletions
+1
View File
@@ -39,6 +39,7 @@ dist/
# docker utils
ontime-db
ontime-external/
ontime-data/
# versioning file
**/ONTIME_VERSION.js
+1 -1
View File
@@ -91,7 +91,7 @@ While it should allow for a generic setup, it might need to be modified to fit y
From the project root, run the following commands
- __Build docker image from__ by running `docker build -t getontime/ontime`
- __Build docker image from__ by running `docker build -t getontime/ontime .`
- __Run docker image from compose__ by running `docker-compose up -d`
Other useful commands
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getontime/cli",
"version": "3.9.0",
"version": "3.9.3",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+25 -29
View File
@@ -1,31 +1,27 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8' />
<base href="/">
<link rel='icon' href='/favicon.ico' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta name='theme-color' content='#101010' />
<meta name='ontime' content='ontime - time keeping for live events' />
<link rel='apple-touch-icon' href='/ontime-logo.png' />
<link
rel='icon'
type='image/png'
href='ontime-logo.png'
/>
<link rel='manifest' href='/site.webmanifest' />
<link rel='manifest' href='/manifest.json' />
<title>ontime</title>
<style>
body,
html {
background-color: rgba(0, 0, 0, 0) !important;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id='root'></div>
<script type='module' src='/src/index.tsx'></script>
</body>
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="/" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#101010" />
<meta name="ontime" content="ontime - time keeping for live events" />
<link rel="apple-touch-icon" href="/ontime-logo.png" />
<link rel="icon" type="image/png" href="/ontime-logo.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="manifest" href="/manifest.json" />
<title>ontime</title>
<style>
body,
html {
background-color: rgba(0, 0, 0, 0) !important;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "3.9.0",
"version": "3.9.3",
"private": true,
"type": "module",
"dependencies": {
@@ -39,7 +39,7 @@
"build": "vite build",
"build:local": "cross-env NODE_ENV=local vite build",
"build:electron": "cross-env NODE_ENV=local vite build",
"build:docker": "cross-env VITE_IS_CLOUD=true vite build",
"build:docker": "cross-env VITE_IS_DOCKER=true vite build",
"build:localdocker": "cross-env NODE_ENV=local vite build",
"lint": "eslint . --quiet",
"test": "vitest",
+2 -1
View File
@@ -11,6 +11,7 @@ import { connectSocket } from './common/utils/socket';
import theme from './theme/theme';
import { TranslationProvider } from './translation/TranslationProvider';
import AppRouter from './AppRouter';
import { baseURI } from './externals';
connectSocket();
@@ -19,7 +20,7 @@ function App() {
<ChakraProvider disableGlobalStyle resetCSS theme={theme}>
<QueryClientProvider client={ontimeQueryClient}>
<AppContextProvider>
<BrowserRouter>
<BrowserRouter basename={baseURI}>
<div className='App'>
<ErrorBoundary>
<TranslationProvider>
@@ -17,7 +17,7 @@ import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
import { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline';
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
import { isLocalhost, serverPort } from '../../../externals';
import { isLocalhost } from '../../../externals';
import { navigatorConstants } from '../../../viewerConfig';
import useClickOutside from '../../hooks/useClickOutside';
import { useElectronEvent } from '../../hooks/useElectronEvent';
@@ -25,7 +25,7 @@ import useInfo from '../../hooks-query/useInfo';
import { useClientStore } from '../../stores/clientStore';
import { useViewOptionsStore } from '../../stores/viewOptions';
import { isKeyEnter } from '../../utils/keyEvent';
import { handleLinks, openLink } from '../../utils/linkUtils';
import { handleLinks, linkToOtherHost, openLink } from '../../utils/linkUtils';
import { cx } from '../../utils/styleUtils';
import { RenameClientModal } from '../client-modal/RenameClientModal';
import CopyTag from '../copy-tag/CopyTag';
@@ -154,7 +154,8 @@ function OtherAddresses(props: OtherAddressesProps) {
return null;
}
const address = `http://${nif.address}:${serverPort}${currentLocation}`;
const address = linkToOtherHost(nif.address, currentLocation);
return (
<CopyTag
key={nif.name}
-30
View File
@@ -1,30 +0,0 @@
/**
* 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}`);
}
+40
View File
@@ -0,0 +1,40 @@
import type { MouseEvent } from 'react';
import { baseURI, serverURL } from '../../externals';
/**
* 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: string) {
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: MouseEvent, location: string) {
// we handle the link manually
event.preventDefault();
const destination = new URL(serverURL);
destination.pathname = baseURI ? `${baseURI}/${location}` : location;
openLink(destination.toString());
}
export function linkToOtherHost(host: string, path?: string) {
const destination = new URL(serverURL);
destination.hostname = host;
if (path) {
destination.pathname = baseURI ? `${baseURI}/${path}` : path;
}
return destination.toString();
}
+53 -9
View File
@@ -1,8 +1,9 @@
import { version } from '../../../package.json';
/**
* This file contains a list of constants that may need to be resolved at runtime
*/
import { version } from '../../../package.json';
export const githubUrl = 'https://www.github.com/cpvalente/ontime';
export const apiRepoLatest = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
export const websiteUrl = 'https://www.getontime.no';
@@ -16,13 +17,56 @@ export const appVersion = version;
export const isProduction = import.meta.env.MODE === 'production';
export const isDev = !isProduction;
export const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
export const isOntimeCloud = Boolean(import.meta.env.VITE_IS_CLOUD);
export const isDockerImage = Boolean(import.meta.env.VITE_IS_DOCKER);
export const isOntimeCloud = window.location.hostname.includes('cloud.getontime.no');
// resolve protocol
const socketProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
// resolve entrypoint URLs
// resolve port
const STATIC_PORT = 4001;
export const serverPort = isProduction ? window.location.port : STATIC_PORT;
export const serverURL = `${window.location.protocol}//${location.hostname}:${serverPort}`;
export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`;
/**
* Resolve base
* @example '' for electron
* @example '/client-hash' for cloud
*/
export const baseURI = resolveBaseURI();
export const serverURL = resolveUrl('http', '');
export const websocketUrl = resolveUrl('ws', 'ws');
function resolveUrl(protocol: 'http' | 'ws', path: string) {
const url = new URL(window.location.origin);
// check if protocol URL is secure
url.protocol = protocol === 'http' ? 'http' : 'ws';
url.protocol += window.location.protocol === 'https:' ? 's' : '';
// make path name relative to the base URI
url.pathname = baseURI ? `${baseURI}/${path}` : path;
// in development mode, we use the React port for UI, but need the requests to target the server
if (isDev) {
// this is used as a fallback port for development
url.port = '4001';
}
const result = url.toString();
// prevent trailing slash
return result.endsWith('/') ? result.slice(0, -1) : result;
}
/**
* Resolves a base URI for a client that is not at the root segment
* ie: https://cloud.getontime.com/client-hash/timer
* This is necessary for ontime cloud and should otherwise not affect the client
*/
function resolveBaseURI(): string {
// in ontime cloud, the base tag is set by the server
const baseHref = document.querySelector('base')?.getAttribute('href');
const base = baseHref ?? '';
// prevent a trailing slash from either an empty base or a base with a trailing slash
if (base.endsWith('/')) {
return base.slice(0, -1);
}
return base;
}
@@ -2,8 +2,8 @@ import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import CopyTag from '../../../../common/components/copy-tag/CopyTag';
import useInfo from '../../../../common/hooks-query/useInfo';
import { openLink } from '../../../../common/utils/linkUtils';
import { isLocalhost, serverPort } from '../../../../externals';
import { linkToOtherHost, openLink } from '../../../../common/utils/linkUtils';
import { isLocalhost } from '../../../../externals';
import style from './NetworkInterfaces.module.scss';
@@ -14,11 +14,11 @@ export default function InfoNif() {
return (
<div className={style.interfaces}>
{data.networkInterfaces?.map((nif) => {
{data.networkInterfaces.map((nif) => {
// interfaces outside localhost wont have access
if (nif.name === 'localhost' && !isLocalhost) return null;
const address = linkToOtherHost(nif.address);
const address = `http://${nif.address}:${serverPort}`;
return (
<CopyTag
key={nif.name}
@@ -3,7 +3,7 @@ import { useEffect } from 'react';
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { usePing } from '../../../../common/hooks/useSocket';
import { socketSendJson } from '../../../../common/utils/socket';
import { isOntimeCloud } from '../../../../externals';
import { isDockerImage } from '../../../../externals';
import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils';
import ClientControlPanel from '../client-control-panel/ClientControlPanel';
@@ -19,7 +19,7 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
<>
<Panel.Header>Network</Panel.Header>
<Panel.Section>
{isOntimeCloud && <OntimeCloudStats />}
{isDockerImage && <OntimeCloudStats />}
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
</Panel.Section>
<InfoNif />
@@ -6,6 +6,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { PROJECT_LIST } from '../../../../common/api/constants';
import { createProject } from '../../../../common/api/db';
import { maybeAxiosError } from '../../../../common/api/utils';
import { documentationUrl, websiteUrl } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import style from './ProjectPanel.module.scss';
@@ -118,7 +119,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
<Input
variant='ontime-filled'
size='sm'
placeholder='www.getontime.no'
placeholder={websiteUrl}
autoComplete='off'
{...register('publicUrl')}
/>
@@ -140,7 +141,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
<Input
variant='ontime-filled'
size='sm'
placeholder='http://docs.getontime.no'
placeholder={documentationUrl}
autoComplete='off'
{...register('backstageUrl')}
/>
@@ -10,6 +10,7 @@ import { postProjectData, uploadProjectLogo } from '../../../../common/api/proje
import { maybeAxiosError } from '../../../../common/api/utils';
import useProjectData from '../../../../common/hooks-query/useProjectData';
import { validateLogo } from '../../../../common/utils/uploadUtils';
import { documentationUrl, websiteUrl } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import style from './ProjectPanel.module.scss';
@@ -203,7 +204,7 @@ export default function ProjectData() {
<Input
variant='ontime-filled'
size='sm'
placeholder='www.getontime.no'
placeholder={websiteUrl}
autoComplete='off'
{...register('publicUrl')}
/>
@@ -225,7 +226,7 @@ export default function ProjectData() {
<Input
variant='ontime-filled'
size='sm'
placeholder='http://docs.getontime.no'
placeholder={documentationUrl}
autoComplete='off'
{...register('backstageUrl')}
/>
@@ -27,6 +27,7 @@
}
.tableContainer {
height: 350px;
max-height: 350px;
overflow-y: auto;
}
@@ -149,6 +149,7 @@ $orange-active: #f60;
grid-area: schedule;
font-family: monospace;
margin-right: 2vw;
overflow: hidden;
}
.onAir {
+2 -1
View File
@@ -11,6 +11,7 @@ const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN;
const isDev = process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'development';
export default defineConfig({
base: './', // Ontime cloud: we use relative paths to allow them to reference a dynamic base set at runtime
plugins: [
react(),
svgrPlugin(),
@@ -33,7 +34,7 @@ export default defineConfig({
}),
compression({
algorithm: 'brotliCompress',
exclude: /\.(html)$/, // Exclude HTML files from compression so we can change the base property at runtime
exclude: /\.(html)$/, // Ontime cloud: Exclude HTML files from compression so we can change the base property at runtime
}),
],
server: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "3.9.0",
"version": "3.9.3",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server",
"type": "module",
"main": "src/index.ts",
"version": "3.9.0",
"version": "3.9.3",
"exports": "./src/index.js",
"dependencies": {
"@googleapis/sheets": "^5.0.5",
+2 -2
View File
@@ -47,8 +47,8 @@ export class SocketServer implements IAdapter {
this.wss = null;
}
init(server: Server) {
this.wss = new WebSocketServer({ path: '/ws', server, maxPayload: this.MAX_PAYLOAD });
init(server: Server, prefix?: string) {
this.wss = new WebSocketServer({ path: `${prefix}/ws`, server, maxPayload: this.MAX_PAYLOAD });
this.wss.on('connection', (ws) => {
const clientId = generateId();
+22 -16
View File
@@ -6,10 +6,10 @@ import expressStaticGzip from 'express-static-gzip';
import http, { type Server } from 'http';
import cors from 'cors';
import serverTiming from 'server-timing';
import { extname, resolve } from 'path';
import { extname } from 'node:path';
// import utils
import { publicDir, srcDir } from './setup/index.js';
import { publicDir, srcDir, srcFiles } from './setup/index.js';
import { environment, isProduction, updateRouterPrefix } from './externals.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
@@ -53,8 +53,14 @@ if (!canLog) {
console.log(`Ontime public directory at ${publicDir.root} `);
}
// calls an update to the client router prefix
updateRouterPrefix();
/**
* When running in Ontime cloud, the client is not at the root segment
* ie: https://cloud.getontime.com/client-hash/timer
* This means:
* - changing the base path in the index.html file
* - prepending all express routes with the given prefix
*/
const prefix = updateRouterPrefix();
// Create express APP
const app = express();
@@ -75,20 +81,20 @@ app.use(express.urlencoded({ extended: true }));
app.use(express.json({ limit: '1mb' }));
// Implement route endpoints
app.use('/data', appRouter); // router for application data
app.use('/api', integrationRouter); // router for integrations
app.use(`${prefix}/data`, appRouter); // router for application data
app.use(`${prefix}/api`, integrationRouter); // router for integrations
// serve static external files
app.use('/external', express.static(publicDir.externalDir));
app.use('/user', express.static(publicDir.userDir));
// if the user reaches to the root, we show a 404
app.use('/external', (req, res) => {
app.use(`${prefix}/external`, express.static(publicDir.externalDir));
app.use(`${prefix}/external`, (req, res) => {
// if the user reaches to the root, we show a 404
res.status(404).send(`${req.originalUrl} not found`);
});
app.use(`${prefix}/user`, express.static(publicDir.userDir));
// serve static - react, in dev/test mode we fetch the React app from module
app.use(
prefix,
expressStaticGzip(srcDir.clientDir, {
enableBrotli: true,
orderPreference: ['br'],
@@ -111,8 +117,8 @@ app.use(
}),
);
app.get('*', (_req, res) => {
res.sendFile(resolve(srcDir.clientDir, 'index.html'));
app.get(`${prefix}/*`, (_req, res) => {
res.sendFile(srcFiles.clientIndexHtml);
});
// Implement catch all
@@ -176,7 +182,7 @@ export const startServer = async (
const { serverPort } = getDataProvider().getSettings();
expressServer = http.createServer(app);
socket.init(expressServer);
socket.init(expressServer, prefix);
/**
* Module initialises the services and provides initial payload for the store
@@ -221,10 +227,10 @@ export const startServer = async (
expressServer.listen(serverPort, '0.0.0.0', () => {
const nif = getNetworkInterfaces();
consoleSuccess(`Local: http://localhost:${serverPort}/editor`);
consoleSuccess(`Local: http://localhost:${serverPort}${prefix}/editor`);
for (const key in nif) {
const address = nif[key].address;
consoleSuccess(`Network: http://${address}:${serverPort}/editor`);
consoleSuccess(`Network: http://${address}:${serverPort}${prefix}/editor`);
}
});
@@ -37,6 +37,14 @@ export class SimpleTimer {
return this.state;
}
public addTime(millis: number): SimpleTimerState {
this.state.duration += millis;
// the value of current will be overridden when update is called,
// but if we are in pause or stop state it will not be changed so we do it here
this.state.current += millis;
return this.state;
}
public setDirection(direction: SimpleDirection, timeNow: number): SimpleTimerState {
// if we are playing, we need to reset the targets
if (this.state.playback === SimplePlayback.Start) {
@@ -177,5 +177,60 @@ describe('SimpleTimer count-down', () => {
playback: SimplePlayback.Start,
});
});
test('adding time affects final result', () => {
timer.reset();
timer.setTime(1000);
timer.start(0);
timer.update(100);
expect(timer.state).toMatchObject({ current: 900, duration: 1000 });
timer.addTime(1000);
timer.update(200);
expect(timer.state).toMatchObject({ current: 1800, duration: 2000 });
timer.update(300);
expect(timer.state).toMatchObject({ current: 1700, duration: 2000 });
timer.stop();
expect(timer.state).toMatchObject({ current: 1000, duration: 1000 });
});
test('adding time affects paused timer', () => {
timer.reset();
timer.setTime(1000);
timer.start(0);
timer.update(100);
expect(timer.state).toMatchObject({ current: 900, duration: 1000 });
timer.pause(200);
expect(timer.state).toMatchObject({ current: 900, duration: 1000 });
timer.addTime(1000);
timer.update(200);
expect(timer.state).toMatchObject({ current: 1900, duration: 2000 });
timer.start(300);
expect(timer.state).toMatchObject({ current: 1800, duration: 2000 });
});
test('adding time affects stopped timer, but returns to initial valuses when stopped again', () => {
timer.reset();
timer.setTime(1000);
expect(timer.state).toMatchObject({ current: 1000, duration: 1000 });
timer.addTime(1000);
expect(timer.state).toMatchObject({ current: 2000, duration: 2000 });
timer.start(0);
timer.update(100);
expect(timer.state).toMatchObject({ current: 1900, duration: 2000 });
timer.stop();
expect(timer.state).toMatchObject({ current: 1000, duration: 1000 });
});
});
});
+10 -8
View File
@@ -3,7 +3,8 @@
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { srcFiles } from './setup/index.js';
// =================================================
// resolve running environment
@@ -19,19 +20,20 @@ export const isProduction = isDocker || (env === 'production' && !isTest);
* This is only needed in the cloud environment where the client is not at the root segment
* ie: https://cloud.getontime.com/client-hash/timer
*/
export function updateRouterPrefix(prefix: string | undefined = process.env.ROUTER_PREFIX) {
export function updateRouterPrefix(prefix: string | undefined = process.env.ROUTER_PREFIX): string {
if (!prefix) {
return;
return '';
}
const indexFile = resolve('.', 'client', 'index.html');
try {
const data = readFileSync(indexFile, { encoding: 'utf-8', flag: 'r' }).replace(
/<base href="[^"]*">/g,
`<base href="${prefix}>"`,
const data = readFileSync(srcFiles.clientIndexHtml, { encoding: 'utf-8', flag: 'r' }).replace(
'<base href="/" />',
`<base href="/${prefix}/" />`,
);
writeFileSync(indexFile, data, { encoding: 'utf-8', flag: 'w' });
writeFileSync(srcFiles.clientIndexHtml, data, { encoding: 'utf-8', flag: 'w' });
} catch (_error) {
/** unhandled */
}
return `/${prefix}`;
}
@@ -1,4 +1,4 @@
import { SimpleDirection, SimpleTimerState } from 'ontime-types';
import { SimpleDirection, SimplePlayback, SimpleTimerState } from 'ontime-types';
import { SimpleTimer } from '../../classes/simple-timer/SimpleTimer.js';
import { eventStore } from '../../stores/EventStore.js';
@@ -59,7 +59,11 @@ export class AuxTimerService {
@broadcastReturn
addTime(millis: number) {
return this.timer.setTime(this.timer.state.current + millis);
if (this.timer.state.playback === SimplePlayback.Start) {
this.timer.addTime(millis);
return this.timer.update(this.getTime());
}
return this.timer.addTime(millis);
}
@broadcastReturn
+2
View File
@@ -77,6 +77,8 @@ export const srcDir = {
} as const;
export const srcFiles = {
/** Path to start index.html */
clientIndexHtml: join(srcDir.clientDir, 'index.html'),
/** Path to bundled CSS */
cssOverride: join(srcDir.root, config.user, config.styles.directory, config.styles.filename),
/** Path to bundled external readme */
@@ -1,33 +0,0 @@
import { cleanURL } from '../url.js';
describe('url is correctly formatted', () => {
it('has no leading spaces', () => {
const test = ' http://testing';
const expected = 'http://testing';
expect(cleanURL(test)).toBe(expected);
});
it('has no trailing spaces', () => {
const test = 'http://testing ';
const expected = 'http://testing';
expect(cleanURL(test)).toBe(expected);
});
it('doesnt contain spaces', () => {
const test = 'http://t e s t i n g';
const expected = 'http://t%20e%20s%20t%20i%20n%20g';
expect(cleanURL(test)).toBe(expected);
});
it('only contains allowed characters', () => {
const test = 'http://<>[]{}|^';
const expected = 'http://';
expect(cleanURL(test)).toBe(expected);
});
it('begins with http://', () => {
const test = 'ontime.com';
const expected = 'http://ontime.com';
expect(cleanURL(test)).toBe(expected);
});
});
-20
View File
@@ -1,20 +0,0 @@
/**
* @description Cleans given url
* @param {string} url - URL to be checked
* @returns {string} Sanitized url
*/
export const cleanURL = (url: string): string => {
// trim whitespaces
let sanitised = url.trim();
// clear any whitespaces
sanitised = sanitised.split(' ').join('%20');
// contain only allowed characters
sanitised = sanitised.replace(/([@\s<>[\]{}|\\^])+/g, '');
// starts with http://
if (!sanitised.startsWith('http://')) sanitised = `http://${sanitised}`;
return sanitised;
};
-2
View File
@@ -1,5 +1,3 @@
version: "3"
services:
ontime:
container_name: ontime
+4
View File
@@ -4,6 +4,10 @@ const fileToUpload = 'e2e/tests/fixtures/test-db.json';
test('project file upload', async ({ page }) => {
await page.goto('http://localhost:4001/editor');
// close the welcome modal if it is open
await page.keyboard.down('Escape');
await page.getByRole('button', { name: 'Edit' }).click();
await page.getByRole('button', { name: 'Clear rundown' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "3.9.0",
"version": "3.9.3",
"description": "Time keeping for live events",
"keywords": [
"ontime",