Compare commits

..

5 Commits

Author SHA1 Message Date
arc-alex 6c460988f1 fix test 2025-05-28 17:06:08 +02:00
arc-alex 00dfdb6d9f option to hide custom fields 2025-05-28 17:02:26 +02:00
arc-alex 0ff5f1b79b deffiretiate main and custom edits 2025-05-28 17:02:26 +02:00
arc-alex 549c291f56 bump version 2025-05-28 08:15:03 +02:00
arc-alex d89309a120 lock cusheet edits 2025-05-27 15:33:39 +02:00
61 changed files with 899 additions and 1970 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

+6 -6
View File
@@ -14,12 +14,12 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 20
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v3
with: with:
version: 10 version: 9
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -56,12 +56,12 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 20
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v3
with: with:
version: 10 version: 9
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -89,12 +89,12 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 20
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v3
with: with:
version: 10 version: 9
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
+2 -2
View File
@@ -20,13 +20,13 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 20
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v3
with: with:
version: 10 version: 9
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
+4 -4
View File
@@ -17,12 +17,12 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 20
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v3
with: with:
version: 10 version: 9
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -73,12 +73,12 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 20
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v3
with: with:
version: 10 version: 9
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
+1 -1
View File
@@ -1 +1 @@
v22.15.1 v20.15.1
+2 -2
View File
@@ -9,8 +9,8 @@ Ontime consists of 3 distinct parts
The steps below will assume you have locally installed the necessary dependencies. The steps below will assume you have locally installed the necessary dependencies.
Other dependencies will be installed as part of the setup Other dependencies will be installed as part of the setup
- __node__ (~22) - __node__ (~20)
- __pnpm__ (~10) - __pnpm__ (~9)
- __docker__ (only necessary to run and build docker images) - __docker__ (only necessary to run and build docker images)
## LOCAL DEVELOPMENT ## LOCAL DEVELOPMENT
+3 -3
View File
@@ -1,13 +1,13 @@
FROM node:22-bullseye AS builder FROM node:20-bullseye AS builder
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN npm install -g pnpm@10.11.0 RUN npm install -g pnpm@9.5.0
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils install --config.dedupe-peer-dependents=false --frozen-lockfile RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils install --config.dedupe-peer-dependents=false --frozen-lockfile
RUN pnpm --filter=ontime-ui --filter=ontime-server run build:docker RUN pnpm --filter=ontime-ui --filter=ontime-server run build:docker
FROM node:22-alpine FROM node:20-alpine
# Set environment variables # Set environment variables
# Environment Variable to signal that we are running production # Environment Variable to signal that we are running production
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getontime/cli", "name": "@getontime/cli",
"version": "3.16.1", "version": "3.16.1-alpha.2",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+6 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "3.16.1", "version": "3.16.1-alpha.2",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@@ -72,7 +72,7 @@
"@types/react-dom": "^18.0.10", "@types/react-dom": "^18.0.10",
"@typescript-eslint/eslint-plugin": "catalog:", "@typescript-eslint/eslint-plugin": "catalog:",
"@typescript-eslint/parser": "catalog:", "@typescript-eslint/parser": "catalog:",
"@vitejs/plugin-react": "4.5.1", "@vitejs/plugin-react": "^4.2.1",
"eslint": "catalog:", "eslint": "catalog:",
"eslint-config-prettier": "catalog:", "eslint-config-prettier": "catalog:",
"eslint-plugin-jest": "^28.6.0", "eslint-plugin-jest": "^28.6.0",
@@ -86,10 +86,10 @@
"prettier": "catalog:", "prettier": "catalog:",
"sass": "^1.57.1", "sass": "^1.57.1",
"typescript": "catalog:", "typescript": "catalog:",
"vite": "6.3.1", "vite": "^5.2.11",
"vite-plugin-compression2": "1.4.0", "vite-plugin-compression2": "^1.3.3",
"vite-plugin-svgr": "4.3.0", "vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "5.1.4", "vite-tsconfig-paths": "^4.3.1",
"vitest": "catalog:" "vitest": "catalog:"
} }
} }
+10 -1
View File
@@ -20,8 +20,17 @@ export async function generateUrl(
baseUrl: string, baseUrl: string,
path: string, path: string,
lock: boolean, lock: boolean,
lockMainFields: boolean,
lockCustomFields: boolean,
authenticate: boolean, authenticate: boolean,
): Promise<string> { ): Promise<string> {
const res = await axios.post(`${sessionPath}/url`, { baseUrl, path, lock, authenticate }); const res = await axios.post(`${sessionPath}/url`, {
baseUrl,
path,
lock,
lockMainFields,
lockCustomFields,
authenticate,
});
return res.data.url; return res.data.url;
} }
@@ -25,4 +25,4 @@ export const AutoTextArea = (props: TextareaProps & { inputref: RefObject<unknow
{...props} {...props}
/> />
); );
}; };
@@ -1,7 +1,6 @@
import { IoChevronDown } from 'react-icons/io5'; import { IoChevronDown } from 'react-icons/io5';
import { useLocalStorage } from '@mantine/hooks'; import { useLocalStorage } from '@mantine/hooks';
import { baseURI } from '../../../externals';
import { cx } from '../../utils/styleUtils'; import { cx } from '../../utils/styleUtils';
import ParamInput from './ParamInput'; import ParamInput from './ParamInput';
@@ -18,7 +17,7 @@ interface ViewParamsSectionProps {
export default function ViewParamsSection(props: ViewParamsSectionProps) { export default function ViewParamsSection(props: ViewParamsSectionProps) {
const { title, collapsible, options } = props; const { title, collapsible, options } = props;
const [collapsed, setCollapsed] = useLocalStorage({ key: `${baseURI}params-${title}`, defaultValue: false }); const [collapsed, setCollapsed] = useLocalStorage({ key: `params-${title}`, defaultValue: false });
const handleCollapse = () => { const handleCollapse = () => {
if (collapsible) { if (collapsible) {
@@ -1,6 +1,5 @@
import { createContext, PropsWithChildren, useCallback, useEffect, useState } from 'react'; import { createContext, PropsWithChildren, useCallback, useEffect, useState } from 'react';
import { baseURI } from '../../externals';
import useSettings from '../hooks-query/useSettings'; import useSettings from '../hooks-query/useSettings';
interface AppContextType { interface AppContextType {
@@ -27,7 +26,7 @@ export const AppContextProvider = ({ children }: PropsWithChildren) => {
useEffect(() => { useEffect(() => {
if (status === 'pending') return; if (status === 'pending') return;
const previousEditor = sessionStorage.getItem(`${baseURI}${storageKeys.editor}`); const previousEditor = sessionStorage.getItem(storageKeys.editor);
if (previousEditor && previousEditor === data.editorKey) { if (previousEditor && previousEditor === data.editorKey) {
setEditorAuth(true); setEditorAuth(true);
@@ -35,7 +34,7 @@ export const AppContextProvider = ({ children }: PropsWithChildren) => {
setEditorAuth(data.editorKey == null || data.editorKey === ''); setEditorAuth(data.editorKey == null || data.editorKey === '');
} }
const previousOperator = sessionStorage.getItem(`${baseURI}${storageKeys.operator}`); const previousOperator = sessionStorage.getItem(storageKeys.operator);
if (previousOperator && previousOperator === data.operatorKey) { if (previousOperator && previousOperator === data.operatorKey) {
setOperatorAuth(true); setOperatorAuth(true);
} else { } else {
@@ -56,14 +55,14 @@ export const AppContextProvider = ({ children }: PropsWithChildren) => {
if (permission === 'editor') { if (permission === 'editor') {
const correct = isValid(pin, data.editorKey); const correct = isValid(pin, data.editorKey);
if (correct) { if (correct) {
sessionStorage.setItem(`${baseURI}${storageKeys.editor}`, pin); sessionStorage.setItem(storageKeys.editor, pin);
} }
setEditorAuth(correct); setEditorAuth(correct);
return correct; return correct;
} else if (permission === 'operator') { } else if (permission === 'operator') {
const correct = isValid(pin, data.operatorKey); const correct = isValid(pin, data.operatorKey);
if (correct) { if (correct) {
sessionStorage.setItem(`${baseURI}${storageKeys.operator}`, pin); sessionStorage.setItem(storageKeys.operator, pin);
} }
setOperatorAuth(correct); setOperatorAuth(correct);
return correct; return correct;
@@ -41,7 +41,7 @@ export function useFlatRundown() {
// update data whenever the revision changes // update data whenever the revision changes
useEffect(() => { useEffect(() => {
if (data.revision !== -1 || data.revision !== prevRevision) { if (data.revision !== -1 && data.revision !== prevRevision) {
const flatRundown = data.order.map((id) => data.rundown[id]); const flatRundown = data.order.map((id) => data.rundown[id]);
setFlatRunDown(flatRundown); setFlatRunDown(flatRundown);
setPrevRevision(data.revision); setPrevRevision(data.revision);
@@ -1,14 +1,12 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { baseURI } from '../../externals';
export enum AppMode { export enum AppMode {
Run = 'run', Run = 'run',
Edit = 'edit', Edit = 'edit',
Freeze = 'freeze', Freeze = 'freeze',
} }
const appModeKey = `${baseURI}ontime-app-mode`; const appModeKey = 'ontime-app-mode';
function getModeFromSession() { function getModeFromSession() {
return sessionStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit; return sessionStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit;
+1 -3
View File
@@ -1,8 +1,6 @@
import { ClientList } from 'ontime-types'; import { ClientList } from 'ontime-types';
import { create } from 'zustand'; import { create } from 'zustand';
import { baseURI } from '../../externals';
interface ClientStore { interface ClientStore {
name?: string; name?: string;
setName: (newValue: string) => void; setName: (newValue: string) => void;
@@ -17,7 +15,7 @@ interface ClientStore {
setClients: (clients: ClientList) => void; setClients: (clients: ClientList) => void;
} }
const clientNameKey = `${baseURI}ontime-client-name`; const clientNameKey = 'ontime-client-name';
function persistNameInStorage(newValue: string) { function persistNameInStorage(newValue: string) {
localStorage.setItem(clientNameKey, newValue); localStorage.setItem(clientNameKey, newValue);
+2 -4
View File
@@ -1,8 +1,6 @@
// eslint-disable-next-line simple-import-sort/imports
import { create } from 'zustand'; import { create } from 'zustand';
import { booleanFromLocalStorage } from '../utils/localStorage'; import { booleanFromLocalStorage } from '../utils/localStorage';
import { baseURI } from '../../externals';
enum LocalEventKeys { enum LocalEventKeys {
Mirror = 'ontime-view-mirror', Mirror = 'ontime-view-mirror',
@@ -14,11 +12,11 @@ type ViewOptionsStore = {
}; };
export const useViewOptionsStore = create<ViewOptionsStore>()((set) => ({ export const useViewOptionsStore = create<ViewOptionsStore>()((set) => ({
mirror: booleanFromLocalStorage(`${baseURI}${LocalEventKeys.Mirror}`, false), mirror: booleanFromLocalStorage(LocalEventKeys.Mirror, false),
toggleMirror: (newValue?: boolean) => toggleMirror: (newValue?: boolean) =>
set((state) => { set((state) => {
const val = typeof newValue === 'undefined' ? !state.mirror : newValue; const val = typeof newValue === 'undefined' ? !state.mirror : newValue;
localStorage.setItem(`${baseURI}${LocalEventKeys.Mirror}`, String(val)); localStorage.setItem(LocalEventKeys.Mirror, String(val));
return { mirror: val }; return { mirror: val };
}), }),
})); }));
@@ -2,4 +2,4 @@
exports[`cx() > ignores falsy values 1`] = `""`; exports[`cx() > ignores falsy values 1`] = `""`;
exports[`cx() > merges styles 1`] = `"_test_d5d33f _another_d5d33f"`; exports[`cx() > merges styles 1`] = `"_test_98a1e0 _another_98a1e0"`;
@@ -64,13 +64,13 @@ describe('getRouteFromPreset()', () => {
describe('handle url sharing edge cases', () => { describe('handle url sharing edge cases', () => {
it('finds the correct preset when the url contains extra arguments', () => { it('finds the correct preset when the url contains extra arguments', () => {
const location = resolvePath('/demopage?locked=true&token=123'); const location = resolvePath('/demopage?locked=true&token=123');
expect(getRouteFromPreset(location, presets)?.startsWith('timer?user=guest&alias=demopage')).toBeTruthy() expect(getRouteFromPreset(location, presets)?.startsWith('timer?user=guest&alias=demopage')).toBeTruthy();
}) });
it('appends the feature params to the alias', () => { it('appends the feature params to the alias', () => {
const location = resolvePath('/demopage?locked=true&token=123'); const location = resolvePath('/demopage?locked=true&token=123');
expect(getRouteFromPreset(location, presets)).toBe('timer?user=guest&alias=demopage&locked=true&token=123') expect(getRouteFromPreset(location, presets)).toBe('timer?user=guest&alias=demopage&locked=true&token=123');
}) });
}); });
}); });
@@ -79,29 +79,30 @@ describe('generatePathFromPreset()', () => {
['timer?user=guest', 'demopage', 'timer?user=guest&alias=demopage'], ['timer?user=guest', 'demopage', 'timer?user=guest&alias=demopage'],
['timer?user=admin', 'demopage', 'timer?user=admin&alias=demopage'], ['timer?user=admin', 'demopage', 'timer?user=admin&alias=demopage'],
])('generates a path from a preset: %s', (path, alias, expected) => { ])('generates a path from a preset: %s', (path, alias, expected) => {
expect(generatePathFromPreset(path, alias, null, null)).toEqual(expected); expect(generatePathFromPreset(path, alias, null, null, null, null)).toEqual(expected);
}); });
test('appends the feature params to the alias', () => { test('appends the feature params to the alias', () => {
expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123')).toBe('timer?user=guest&alias=demopage&locked=true&token=123'); expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123', null, null)).toBe(
'timer?user=guest&alias=demopage&locked=true&token=123',
);
}); });
}); });
describe('arePathsEquivalent()', () => { describe('arePathsEquivalent()', () => {
it("checks whether the paths match", () => { it('checks whether the paths match', () => {
expect(arePathsEquivalent('demopage', 'timer')).toBeFalsy(); expect(arePathsEquivalent('demopage', 'timer')).toBeFalsy();
expect(arePathsEquivalent('timer', 'timer')).toBeTruthy(); expect(arePathsEquivalent('timer', 'timer')).toBeTruthy();
expect(arePathsEquivalent('timer?user=guest', 'timer?user=guest')).toBeTruthy(); expect(arePathsEquivalent('timer?user=guest', 'timer?user=guest')).toBeTruthy();
}) });
it("checks whether the params match", () => { it('checks whether the params match', () => {
expect(arePathsEquivalent('timer?test=a', 'timer?test=b')).toBeFalsy(); expect(arePathsEquivalent('timer?test=a', 'timer?test=b')).toBeFalsy();
expect(arePathsEquivalent('timer?test=a', 'timer?test=a')).toBeTruthy(); expect(arePathsEquivalent('timer?test=a', 'timer?test=a')).toBeTruthy();
}) });
it("considers edge cases for the url sharing feature", () => { it('considers edge cases for the url sharing feature', () => {
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=b')).toBeFalsy(); expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=b')).toBeFalsy();
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy(); expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy();
}) });
}); });
+24 -6
View File
@@ -45,11 +45,14 @@ export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): str
const locked = searchParams.get('locked'); const locked = searchParams.get('locked');
const token = searchParams.get('token'); const token = searchParams.get('token');
const lmain = searchParams.get('lmain');
const lcustom = searchParams.get('lcustom');
// we need to check if the whole url is an alias // we need to check if the whole url is an alias
const foundPreset = urlPresets.find((preset) => preset.alias === removeTrailingSlash(currentURL) && preset.enabled); const foundPreset = urlPresets.find((preset) => preset.alias === removeTrailingSlash(currentURL) && preset.enabled);
if (foundPreset) { if (foundPreset) {
// if so, we can redirect to the preset path // if so, we can redirect to the preset path
return generatePathFromPreset(foundPreset.pathAndParams, foundPreset.alias, locked, token); return generatePathFromPreset(foundPreset.pathAndParams, foundPreset.alias, locked, token, lmain, lcustom);
} }
// if the current url is not an alias, we check if the alias is in the search parameters // if the current url is not an alias, we check if the alias is in the search parameters
@@ -63,7 +66,7 @@ export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): str
for (const preset of urlPresets) { for (const preset of urlPresets) {
// if the page has a known enabled alias, we check if we need to redirect // if the page has a known enabled alias, we check if we need to redirect
if (preset.alias === presetOnPage && preset.enabled) { if (preset.alias === presetOnPage && preset.enabled) {
const newPath = generatePathFromPreset(preset.pathAndParams, preset.alias, locked, token); const newPath = generatePathFromPreset(preset.pathAndParams, preset.alias, locked, token, lmain, lcustom);
if (!arePathsEquivalent(currentPath, newPath)) { if (!arePathsEquivalent(currentPath, newPath)) {
// if current path is out of date // if current path is out of date
// return new path so we can redirect // return new path so we can redirect
@@ -77,7 +80,14 @@ export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): str
/** /**
* Handles generating a path and search parameters from a preset * Handles generating a path and search parameters from a preset
*/ */
export function generatePathFromPreset(pathAndParams: string, alias: string, locked: string | null, token: string | null ): string { export function generatePathFromPreset(
pathAndParams: string,
alias: string,
locked: string | null,
token: string | null,
lmain: string | null,
lcustom: string | null,
): string {
const path = resolvePath(pathAndParams); const path = resolvePath(pathAndParams);
const searchParams = new URLSearchParams(path.search); const searchParams = new URLSearchParams(path.search);
@@ -93,6 +103,14 @@ export function generatePathFromPreset(pathAndParams: string, alias: string, loc
searchParams.set('token', token); searchParams.set('token', token);
} }
if (lmain) {
searchParams.set('lmain', lmain);
}
if (lcustom) {
searchParams.set('lcustom', lcustom);
}
// return path concatenated without the leading slash // return path concatenated without the leading slash
return `${path.pathname}?${searchParams}`.substring(1); return `${path.pathname}?${searchParams}`.substring(1);
} }
@@ -106,16 +124,16 @@ export function generatePathFromPreset(pathAndParams: string, alias: string, loc
export function arePathsEquivalent(currentPath: string, newPath: string): boolean { export function arePathsEquivalent(currentPath: string, newPath: string): boolean {
const currentUrl = new URL(currentPath, document.location.origin); const currentUrl = new URL(currentPath, document.location.origin);
const newUrl = new URL(newPath, document.location.origin); const newUrl = new URL(newPath, document.location.origin);
// check path // check path
if (currentUrl.pathname !== newUrl.pathname) { if (currentUrl.pathname !== newUrl.pathname) {
return false return false;
} }
// check search params // check search params
// if the params match, we dont need further checks // if the params match, we dont need further checks
if (currentUrl.searchParams.toString() === newUrl.searchParams.toString()) { if (currentUrl.searchParams.toString() === newUrl.searchParams.toString()) {
return true return true;
} }
// if there is no match, we check the edge cases for the url sharing feature // if there is no match, we check the edge cases for the url sharing feature
+2
View File
@@ -31,6 +31,8 @@ declare module '@tanstack/react-table' {
options: { options: {
showDelayedTimes: boolean; showDelayedTimes: boolean;
hideTableSeconds: boolean; hideTableSeconds: boolean;
allowMainEdits: boolean;
allowCustomEdits: boolean;
}; };
} }
} }
@@ -20,6 +20,8 @@ interface GenerateLinkFormOptions {
baseUrl: string; baseUrl: string;
path: string; path: string;
lock: boolean; lock: boolean;
lockMainFields: boolean;
lockCustomFields: boolean;
authenticate: boolean; authenticate: boolean;
} }
@@ -35,13 +37,15 @@ export default function GenerateLinkForm() {
handleSubmit, handleSubmit,
register, register,
setError, setError,
formState: { errors }, formState: { errors, dirtyFields },
} = useForm<GenerateLinkFormOptions>({ } = useForm<GenerateLinkFormOptions>({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
baseUrl: currentHostName, baseUrl: currentHostName,
path: '', path: '',
lock: false, lock: false,
lockMainFields: false,
lockCustomFields: false,
authenticate: false, authenticate: false,
}, },
resetOptions: { resetOptions: {
@@ -53,7 +57,14 @@ export default function GenerateLinkForm() {
try { try {
setFormState('loading'); setFormState('loading');
const baseUrl = linkToOtherHost(options.baseUrl); const baseUrl = linkToOtherHost(options.baseUrl);
const url = await generateUrl(baseUrl, options.path, options.lock, options.authenticate); const url = await generateUrl(
baseUrl,
options.path,
options.lock,
options.lockMainFields,
options.lockCustomFields,
options.authenticate,
);
await copyToClipboard(url); await copyToClipboard(url);
setUrl(url); setUrl(url);
setFormState('success'); setFormState('success');
@@ -119,6 +130,18 @@ export default function GenerateLinkForm() {
/> />
<Switch variant='ontime' size='lg' {...register('lock')} /> <Switch variant='ontime' size='lg' {...register('lock')} />
</Panel.ListItem> </Panel.ListItem>
{dirtyFields.lock && (
<>
<Panel.ListItem>
<Panel.Field title='Lock main field edits' description='Prevent edits to main fields' />
<Switch variant='ontime' size='lg' {...register('lockMainFields')} />
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Lock custom field edits' description='Prevent edits to custom fields' />
<Switch variant='ontime' size='lg' {...register('lockCustomFields')} />
</Panel.ListItem>
</>
)}
<Panel.ListItem> <Panel.ListItem>
<Panel.Field title='Authenticate' description='Whether the URL should be pre-authenticated' /> <Panel.Field title='Authenticate' description='Whether the URL should be pre-authenticated' />
<Switch variant='ontime' size='lg' {...register('authenticate')} /> <Switch variant='ontime' size='lg' {...register('authenticate')} />
@@ -1,7 +1,5 @@
import { ImportCustom, ImportMap } from 'ontime-utils'; import { ImportCustom, ImportMap } from 'ontime-utils';
import { baseURI } from '../../../../../externals';
export type NamedImportMap = typeof namedImportMap; export type NamedImportMap = typeof namedImportMap;
// Record of label and import name // Record of label and import name
@@ -66,11 +64,11 @@ export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
} }
export function persistImportMap(options: NamedImportMap) { export function persistImportMap(options: NamedImportMap) {
localStorage.setItem(`${baseURI}ontime-import-options`, JSON.stringify(options)); localStorage.setItem('ontime-import-options', JSON.stringify(options));
} }
function getPersistImportMap(): unknown { function getPersistImportMap(): unknown {
const options = localStorage.getItem(`${baseURI}ontime-import-options`); const options = localStorage.getItem('ontime-import-options');
if (!options) { if (!options) {
throw new Error('no import options found'); throw new Error('no import options found');
} }
@@ -6,7 +6,6 @@ import { MILLIS_PER_HOUR, MILLIS_PER_SECOND, parseUserTime } from 'ontime-utils'
import TimeInput from '../../../../common/components/input/time-input/TimeInput'; import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import { setPlayback } from '../../../../common/hooks/useSocket'; import { setPlayback } from '../../../../common/hooks/useSocket';
import { baseURI } from '../../../../externals';
import { tooltipDelayMid } from '../../../../ontimeConfig'; import { tooltipDelayMid } from '../../../../ontimeConfig';
import TapButton from '../tap-button/TapButton'; import TapButton from '../tap-button/TapButton';
@@ -18,7 +17,7 @@ interface AddTimeProps {
export default function AddTime(props: AddTimeProps) { export default function AddTime(props: AddTimeProps) {
const { playback } = props; const { playback } = props;
const [time, setTime] = useLocalStorage({ key: `${baseURI}add-time`, defaultValue: 300_000 }); // 5 minutes const [time, setTime] = useLocalStorage({ key: 'add-time', defaultValue: 300_000 }); // 5 minutes
const handleTimeChange = (_field: string, value: string) => { const handleTimeChange = (_field: string, value: string) => {
const newTime = parseUserTime(value); const newTime = parseUserTime(value);
@@ -16,7 +16,7 @@ import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
import CuesheetProgress from './cuesheet-progress/CuesheetProgress'; import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetCols'; import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetCols';
import CuesheetTable from './cuesheet-table/CuesheetTable'; import CuesheetTable from './cuesheet-table/CuesheetTable';
import { cuesheetOptions } from './cuesheet.options'; import { cuesheetOptions, useCuesheetOptions } from './cuesheet.options';
import styles from './CuesheetPage.module.scss'; import styles from './CuesheetPage.module.scss';
@@ -28,8 +28,9 @@ export default function CuesheetPage() {
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure(); const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure(); const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure();
const [eventId, setEventId] = useState<string | null>(null); const [eventId, setEventId] = useState<string | null>(null);
const { hideCustom } = useCuesheetOptions();
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]); const columns = useMemo(() => makeCuesheetColumns(hideCustom ? {} : customFields), [customFields, hideCustom]);
useWindowTitle('Cuesheet'); useWindowTitle('Cuesheet');
@@ -96,7 +96,6 @@ describe('makeTable()', () => {
"Is Public? (x)", "Is Public? (x)",
"Skip?", "Skip?",
"lighting", "lighting",
"Type",
], ],
[ [
"00:00:00", "00:00:00",
@@ -110,7 +109,6 @@ describe('makeTable()', () => {
"x", "x",
"", "",
"", "",
"",
], ],
] ]
`); `);
@@ -1,4 +1,5 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useTableNav } from '@table-nav/react'; import { useTableNav } from '@table-nav/react';
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown, OntimeRundownEntry, TimeField } from 'ontime-types'; import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown, OntimeRundownEntry, TimeField } from 'ontime-types';
@@ -25,6 +26,10 @@ export default function CuesheetTable(props: CuesheetTableProps) {
const { data, columns, showModal } = props; const { data, columns, showModal } = props;
const { updateEvent, updateTimer } = useEventAction(); const { updateEvent, updateTimer } = useEventAction();
const [searchParams] = useSearchParams();
const allowMainEdits = !searchParams.get('lmain');
const allowCustomEdits = !searchParams.get('lcustom');
const { followSelected, showDelayedTimes, hideTableSeconds } = useCuesheetOptions(); const { followSelected, showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } = const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
useColumnManager(columns); useColumnManager(columns);
@@ -77,6 +82,8 @@ export default function CuesheetTable(props: CuesheetTableProps) {
options: { options: {
showDelayedTimes, showDelayedTimes,
hideTableSeconds, hideTableSeconds,
allowMainEdits,
allowCustomEdits,
}, },
}, },
}); });
@@ -29,7 +29,6 @@ interface EventRowProps {
export default memo(EventRow, (prevProps, nextProps) => { export default memo(EventRow, (prevProps, nextProps) => {
return ( return (
prevProps.rowId === nextProps.rowId && prevProps.rowId === nextProps.rowId &&
prevProps.event.id === nextProps.event.id &&
prevProps.event.revision === nextProps.event.revision && prevProps.event.revision === nextProps.event.revision &&
prevProps.eventIndex === nextProps.eventIndex && prevProps.eventIndex === nextProps.eventIndex &&
prevProps.rowIndex === nextProps.rowIndex && prevProps.rowIndex === nextProps.rowIndex &&
@@ -6,12 +6,13 @@ import useReactiveTextInput from '../../../../common/components/input/text-input
interface MultiLineCellProps { interface MultiLineCellProps {
initialValue: string; initialValue: string;
handleUpdate: (newValue: string) => void; handleUpdate: (newValue: string) => void;
allowEdits?: boolean;
} }
export default memo(MultiLineCell); export default memo(MultiLineCell);
function MultiLineCell(props: MultiLineCellProps) { function MultiLineCell(props: MultiLineCellProps) {
const { initialValue, handleUpdate } = props; const { initialValue, handleUpdate, allowEdits } = props;
const ref = useRef<HTMLInputElement | null>(null); const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]); const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
@@ -38,6 +39,7 @@ function MultiLineCell(props: MultiLineCellProps) {
onBlur={onBlur} onBlur={onBlur}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
spellCheck={false} spellCheck={false}
isDisabled={!allowEdits}
/> />
); );
} }
@@ -1,17 +1,18 @@
import { forwardRef, memo, useCallback, useImperativeHandle, useRef } from 'react'; import { forwardRef, memo, useCallback, useImperativeHandle, useRef } from 'react';
import { Input } from '@chakra-ui/react'; import { Input, Text } from '@chakra-ui/react';
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput'; import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
interface SingleLineCellProps { interface SingleLineCellProps {
initialValue: string; initialValue: string;
allowSubmitSameValue?: boolean; allowSubmitSameValue?: boolean;
allowEdits?: boolean;
handleUpdate: (newValue: string) => void; handleUpdate: (newValue: string) => void;
handleCancelUpdate?: () => void; handleCancelUpdate?: () => void;
} }
const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => { const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
const { initialValue, allowSubmitSameValue, handleUpdate, handleCancelUpdate } = props; const { initialValue, allowSubmitSameValue, handleUpdate, handleCancelUpdate, allowEdits } = props;
const ref = useRef<HTMLInputElement | null>(null); const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]); const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
@@ -38,6 +39,14 @@ const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
}; };
}, [ref]); }, [ref]);
if (allowEdits === false) {
return (
<Text ref={ref} size='sm' variant='ontime-transparent' padding={0} fontSize='md'>
{initialValue}
</Text>
);
}
return ( return (
<Input <Input
ref={ref} ref={ref}
@@ -8,6 +8,7 @@ interface TimeInputDurationProps {
initialValue: number; initialValue: number;
lockedValue: boolean; lockedValue: boolean;
delayed?: boolean; delayed?: boolean;
allowEdits?: boolean;
onSubmit: (value: string) => void; onSubmit: (value: string) => void;
} }
@@ -18,7 +19,7 @@ interface ParentFocusableInput extends HTMLInputElement {
export default memo(TimeInputDuration); export default memo(TimeInputDuration);
function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) { function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
const { initialValue, lockedValue, delayed, onSubmit, children } = props; const { initialValue, lockedValue, delayed, onSubmit, children, allowEdits } = props;
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
@@ -86,7 +87,7 @@ function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
const timeString = millisToString(value); const timeString = millisToString(value);
return isEditing ? ( return isEditing && allowEdits ? (
<SingleLineCell <SingleLineCell
ref={inputRef} ref={inputRef}
initialValue={timeString} initialValue={timeString}
@@ -32,7 +32,13 @@ function MakeStart({ getValue, row, table }: CellContext<OntimeRundownEntry, unk
} }
return ( return (
<TimeInput initialValue={startTime} onSubmit={update} lockedValue={isStartLocked} delayed={delayValue !== 0}> <TimeInput
initialValue={startTime}
onSubmit={update}
lockedValue={isStartLocked}
delayed={delayValue !== 0}
allowEdits={table.options.meta?.options.allowMainEdits}
>
{formattedTime} {formattedTime}
<DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(startTime)} /> <DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(startTime)} />
</TimeInput> </TimeInput>
@@ -60,7 +66,13 @@ function MakeEnd({ getValue, row, table }: CellContext<OntimeRundownEntry, unkno
} }
return ( return (
<TimeInput initialValue={endTime} onSubmit={update} lockedValue={isEndLocked} delayed={delayValue !== 0}> <TimeInput
initialValue={endTime}
onSubmit={update}
lockedValue={isEndLocked}
delayed={delayValue !== 0}
allowEdits={table.options.meta?.options.allowMainEdits}
>
{formattedTime} {formattedTime}
<DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(endTime)} /> <DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(endTime)} />
</TimeInput> </TimeInput>
@@ -81,7 +93,12 @@ function MakeDuration({ getValue, row, table }: CellContext<OntimeRundownEntry,
const formattedDuration = formatDuration(duration, false); const formattedDuration = formatDuration(duration, false);
return ( return (
<TimeInput initialValue={duration} onSubmit={update} lockedValue={isDurationLocked}> <TimeInput
initialValue={duration}
onSubmit={update}
lockedValue={isDurationLocked}
allowEdits={table.options.meta?.options.allowMainEdits}
>
{formattedDuration} {formattedDuration}
</TimeInput> </TimeInput>
); );
@@ -103,7 +120,13 @@ function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEnt
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? ''; const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />; return (
<MultiLineCell
initialValue={initialValue}
handleUpdate={update}
allowEdits={table.options.meta?.options.allowMainEdits}
/>
);
} }
function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) { function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
@@ -140,7 +163,13 @@ function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEn
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? ''; const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
return <SingleLineCell initialValue={initialValue} handleUpdate={update} />; return (
<SingleLineCell
initialValue={initialValue}
handleUpdate={update}
allowEdits={table.options.meta?.options.allowMainEdits}
/>
);
} }
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) { function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
@@ -158,7 +187,13 @@ function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry,
} }
const initialValue = event.custom[column.id] ?? ''; const initialValue = event.custom[column.id] ?? '';
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />; return (
<MultiLineCell
initialValue={initialValue}
handleUpdate={update}
allowEdits={table.options.meta?.options.allowCustomEdits}
/>
);
} }
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] { export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] {
@@ -3,18 +3,13 @@ import { useLocalStorage } from '@mantine/hooks';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { OntimeRundownEntry } from 'ontime-types'; import { OntimeRundownEntry } from 'ontime-types';
import { baseURI } from '../../../externals';
export default function useColumnManager(columns: ColumnDef<OntimeRundownEntry>[]) { export default function useColumnManager(columns: ColumnDef<OntimeRundownEntry>[]) {
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: `${baseURI}table-hidden`, defaultValue: {} }); const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} });
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({ const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
key: `${baseURI}table-order`, key: 'table-order',
defaultValue: columns.map((col) => col.id as string), defaultValue: columns.map((col) => col.id as string),
}); });
const [columnSizing, setColumnSizing] = useLocalStorage({ const [columnSizing, setColumnSizing] = useLocalStorage({ key: 'table-sizes', defaultValue: {} });
key: `${baseURI}table-sizes`,
defaultValue: {},
});
// if the columns change, we update the dataset // if the columns change, we update the dataset
useEffect(() => { useEffect(() => {
@@ -49,6 +49,13 @@ export const cuesheetOptions: ViewOption[] = [
type: 'boolean', type: 'boolean',
defaultValue: false, defaultValue: false,
}, },
{
id: 'hideCustomColumns',
title: 'Hide custom data columns',
description: 'Whether the hide the custom data in the table',
type: 'boolean',
defaultValue: false,
},
], ],
}, },
{ {
@@ -81,6 +88,7 @@ type CuesheetOptions = {
hideIndexColumn: boolean; hideIndexColumn: boolean;
showDelayedTimes: boolean; showDelayedTimes: boolean;
hideDelays: boolean; hideDelays: boolean;
hideCustom: boolean;
}; };
/** /**
@@ -97,6 +105,7 @@ function getOptionsFromParams(searchParams: URLSearchParams): CuesheetOptions {
hideIndexColumn: isStringBoolean(searchParams.get('hideIndexColumn')), hideIndexColumn: isStringBoolean(searchParams.get('hideIndexColumn')),
showDelayedTimes: isStringBoolean(searchParams.get('showDelayedTimes')), showDelayedTimes: isStringBoolean(searchParams.get('showDelayedTimes')),
hideDelays: isStringBoolean(searchParams.get('hideDelays')), hideDelays: isStringBoolean(searchParams.get('hideDelays')),
hideCustom: isStringBoolean(searchParams.get('hideCustomColumns')),
}; };
} }
@@ -59,7 +59,6 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, custo
'isPublic', 'isPublic',
'skip', 'skip',
...customFieldKeys, ...customFieldKeys,
'type',
]; ];
const fieldTitles = [ const fieldTitles = [
@@ -74,7 +73,6 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, custo
'Is Public? (x)', 'Is Public? (x)',
'Skip?', 'Skip?',
...customFieldLabels, ...customFieldLabels,
'Type',
]; ];
// add header row to data // add header row to data
+3 -3
View File
@@ -70,9 +70,9 @@ export default defineConfig({
preprocessorOptions: { preprocessorOptions: {
scss: { scss: {
additionalData: ` additionalData: `
@use '@/theme/ontimeColours' as *; @use './src/theme/ontimeColours' as *;
@use '@/theme/ontimeStyles' as *; @use './src/theme/ontimeStyles' as *;
@use '@/theme/mixins' as *; @use './src/theme/mixins' as *;
`, `,
}, },
}, },
+6 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-electron", "name": "ontime-electron",
"version": "3.16.1", "version": "3.16.1-alpha.2",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
@@ -12,8 +12,8 @@
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "src/main.js", "main": "src/main.js",
"devDependencies": { "devDependencies": {
"electron": "36.3.1", "electron": "^31.2.0",
"electron-builder": "26.0.12", "electron-builder": "^24.13.3",
"eslint": "catalog:", "eslint": "catalog:",
"eslint-config-prettier": "catalog:", "eslint-config-prettier": "catalog:",
"prettier": "catalog:", "prettier": "catalog:",
@@ -38,7 +38,9 @@
"icon": "icon.icns" "icon": "icon.icns"
}, },
"mac": { "mac": {
"notarize": true, "notarize": {
"teamId": "MDAU6QK6R4"
},
"hardenedRuntime": true, "hardenedRuntime": true,
"gatekeeperAssess": false, "gatekeeperAssess": false,
"entitlements": "./entitlements.plist", "entitlements": "./entitlements.plist",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server", "name": "ontime-server",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"version": "3.16.1", "version": "3.16.1-alpha.2",
"exports": "./src/index.js", "exports": "./src/index.js",
"dependencies": { "dependencies": {
"@googleapis/sheets": "^5.0.5", "@googleapis/sheets": "^5.0.5",
+1 -1
View File
@@ -5,7 +5,7 @@ import * as dgram from 'node:dgram';
import { logger } from '../classes/Logger.js'; import { logger } from '../classes/Logger.js';
import { dispatchFromAdapter } from '../api-integration/integration.controller.js'; import { dispatchFromAdapter } from '../api-integration/integration.controller.js';
import { isOntimeCloud } from '../setup/environment.js'; import { isOntimeCloud } from '../externals.js';
import { integrationPayloadFromPath } from './utils/parse.js'; import { integrationPayloadFromPath } from './utils/parse.js';
import type { IAdapter } from './IAdapter.js'; import type { IAdapter } from './IAdapter.js';
@@ -12,7 +12,7 @@ import { getPropertyFromPath } from 'ontime-utils';
import { logger } from '../../classes/Logger.js'; import { logger } from '../../classes/Logger.js';
import { getState, type RuntimeState } from '../../stores/runtimeState.js'; import { getState, type RuntimeState } from '../../stores/runtimeState.js';
import { isOntimeCloud } from '../../setup/environment.js'; import { isOntimeCloud } from '../../externals.js';
import { emitOSC } from './clients/osc.client.js'; import { emitOSC } from './clients/osc.client.js';
import { emitHTTP } from './clients/http.client.js'; import { emitHTTP } from './clients/http.client.js';
@@ -3,17 +3,26 @@ import { generateAuthenticatedUrl } from '../session.service.js';
describe('generateAuthenticatedUrl()', () => { describe('generateAuthenticatedUrl()', () => {
describe('for local IP addresses', () => { describe('for local IP addresses', () => {
it('generates a link without locking or authentication', () => { it('generates a link without locking or authentication', () => {
const localhostNotLocked = generateAuthenticatedUrl('http://localhost:3000', 'timer', false, false); const localhostNotLocked = generateAuthenticatedUrl('http://localhost:3000', 'timer', false, false, false, false);
expect(localhostNotLocked.toString()).toBe('http://localhost:3000/timer'); expect(localhostNotLocked.toString()).toBe('http://localhost:3000/timer');
}); });
it('generates a link with IP locking enabled', () => { it('generates a link with IP locking enabled', () => {
const ipLocked = generateAuthenticatedUrl('http://192.168.10.173:4001', 'timer', true, false); const ipLocked = generateAuthenticatedUrl('http://192.168.10.173:4001', 'timer', true, false, false, false);
expect(ipLocked.toString()).toBe('http://192.168.10.173:4001/timer?locked=true'); expect(ipLocked.toString()).toBe('http://192.168.10.173:4001/timer?locked=true');
}); });
it('generates a link with authentication token and IP locking', () => { it('generates a link with authentication token and IP locking', () => {
const withAuth = generateAuthenticatedUrl('http://192.168.10.173:4001', 'timer', true, true, undefined, '1234'); const withAuth = generateAuthenticatedUrl(
'http://192.168.10.173:4001',
'timer',
true,
false,
false,
true,
undefined,
'1234',
);
expect(withAuth.toString()).toBe('http://192.168.10.173:4001/timer?token=1234&locked=true'); expect(withAuth.toString()).toBe('http://192.168.10.173:4001/timer?token=1234&locked=true');
}); });
}); });
@@ -25,13 +34,23 @@ describe('generateAuthenticatedUrl()', () => {
'timer', 'timer',
false, false,
false, false,
false,
false,
'prefix', 'prefix',
); );
expect(cloudNotLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer'); expect(cloudNotLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer');
}); });
it('generates a link with IP locking enabled', () => { it('generates a link with IP locking enabled', () => {
const ipLocked = generateAuthenticatedUrl('https://cloud.getontime.no/prefix', 'timer', true, false, 'prefix'); const ipLocked = generateAuthenticatedUrl(
'https://cloud.getontime.no/prefix',
'timer',
true,
false,
false,
false,
'prefix',
);
expect(ipLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer?locked=true'); expect(ipLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer?locked=true');
}); });
@@ -40,6 +59,8 @@ describe('generateAuthenticatedUrl()', () => {
'https://cloud.getontime.no/prefix', 'https://cloud.getontime.no/prefix',
'timer', 'timer',
true, true,
false,
false,
true, true,
'prefix', 'prefix',
'1234', '1234',
@@ -31,6 +31,8 @@ export async function generateUrl(req: Request, res: Response<GetUrl | ErrorResp
req.body.baseUrl, req.body.baseUrl,
req.body.path, req.body.path,
req.body.lock, req.body.lock,
req.body.lockMainFields,
req.body.lockCustomFields,
req.body.authenticate, req.body.authenticate,
); );
res.status(200).send({ url: url.toString() }); res.status(200).send({ url: url.toString() });
@@ -10,7 +10,6 @@ import { getNetworkInterfaces } from '../../utils/network.js';
import { getTimezoneLabel } from '../../utils/time.js'; import { getTimezoneLabel } from '../../utils/time.js';
import { password, routerPrefix } from '../../externals.js'; import { password, routerPrefix } from '../../externals.js';
import { hashPassword } from '../../utils/hash.js'; import { hashPassword } from '../../utils/hash.js';
import { ONTIME_VERSION } from '../../ONTIME_VERSION.js';
const startedAt = new Date(); const startedAt = new Date();
@@ -29,7 +28,6 @@ export async function getSessionStats(): Promise<SessionStats> {
projectName, projectName,
playback, playback,
timezone: getTimezoneLabel(startedAt), timezone: getTimezoneLabel(startedAt),
version: ONTIME_VERSION,
}; };
} }
@@ -61,6 +59,8 @@ export function generateAuthenticatedUrl(
baseUrl: string, baseUrl: string,
path: string, path: string,
lock: boolean, lock: boolean,
lockMainFields: boolean,
lockCustomFields: boolean,
authenticate: boolean, authenticate: boolean,
prefix = routerPrefix, prefix = routerPrefix,
hash = hashedPassword, hash = hashedPassword,
@@ -74,5 +74,11 @@ export function generateAuthenticatedUrl(
if (lock) { if (lock) {
url.searchParams.append('locked', 'true'); url.searchParams.append('locked', 'true');
} }
if (lockMainFields) {
url.searchParams.append('lmain', 'true');
}
if (lockCustomFields) {
url.searchParams.append('lcustom', 'true');
}
return url; return url;
} }
@@ -5,6 +5,8 @@ export const validateGenerateUrl = [
body('baseUrl').exists().isString().notEmpty().trim(), body('baseUrl').exists().isString().notEmpty().trim(),
body('path').exists().isString().trim(), body('path').exists().isString().trim(),
body('lock').exists().isBoolean(), body('lock').exists().isBoolean(),
body('lockMainFields').exists().isBoolean(),
body('lockCustomFields').exists().isBoolean(),
body('authenticate').exists().isBoolean(), body('authenticate').exists().isBoolean(),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
@@ -3,7 +3,7 @@ import { getErrorMessage, obfuscate } from 'ontime-utils';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { isDocker } from '../../setup/environment.js'; import { isDocker } from '../../externals.js';
import { failEmptyObjects } from '../../utils/routerUtils.js'; import { failEmptyObjects } from '../../utils/routerUtils.js';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import * as appState from '../../services/app-state-service/AppStateService.js'; import * as appState from '../../services/app-state-service/AppStateService.js';
+1 -2
View File
@@ -9,8 +9,7 @@ import cookieParser from 'cookie-parser';
// import utils // import utils
import { publicDir, srcDir } from './setup/index.js'; import { publicDir, srcDir } from './setup/index.js';
import { environment, isProduction } from './setup/environment.js'; import { environment, isProduction, updateRouterPrefix } from './externals.js';
import { updateRouterPrefix } from './externals.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js'; import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js'; import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
+1 -1
View File
@@ -4,7 +4,7 @@ import { generateId, millisToString } from 'ontime-utils';
import { clock } from '../services/Clock.js'; import { clock } from '../services/Clock.js';
import { socket } from '../adapters/WebsocketAdapter.js'; import { socket } from '../adapters/WebsocketAdapter.js';
import { consoleSubdued, consoleError } from '../utils/console.js'; import { consoleSubdued, consoleError } from '../utils/console.js';
import { isProduction } from '../setup/environment.js'; import { isProduction } from '../externals.js';
class Logger { class Logger {
private queue: Log[]; private queue: Log[];
@@ -14,7 +14,7 @@ import { JSONFilePreset } from 'lowdb/node';
import { isPath } from '../../utils/fileManagement.js'; import { isPath } from '../../utils/fileManagement.js';
import { shouldCrashDev } from '../../utils/development.js'; import { shouldCrashDev } from '../../utils/development.js';
import { isTest } from '../../setup/environment.js'; import { isTest } from '../../externals.js';
import { safeMerge } from './DataProvider.utils.js'; import { safeMerge } from './DataProvider.utils.js';
+8
View File
@@ -7,6 +7,14 @@ import { readFileSync, writeFileSync } from 'node:fs';
import { srcFiles } from './setup/index.js'; import { srcFiles } from './setup/index.js';
// ================================================= // =================================================
// resolve running environment
const env = process.env.NODE_ENV || 'production';
export const isTest = Boolean(process.env.IS_TEST);
export const environment = isTest ? 'test' : env;
export const isDocker = env === 'docker';
export const isProduction = isDocker || (env === 'production' && !isTest);
export const isOntimeCloud = Boolean(process.env.IS_CLOUD);
export const password = process.env.SESSION_PASSWORD; export const password = process.env.SESSION_PASSWORD;
export const routerPrefix = process.env.ROUTER_PREFIX; export const routerPrefix = process.env.ROUTER_PREFIX;
@@ -2,7 +2,7 @@ import { Low } from 'lowdb';
import { JSONFile } from 'lowdb/node'; import { JSONFile } from 'lowdb/node';
import { publicFiles } from '../../setup/index.js'; import { publicFiles } from '../../setup/index.js';
import { isTest } from '../../setup/environment.js'; import { isTest } from '../../externals.js';
import { isPath } from '../../utils/fileManagement.js'; import { isPath } from '../../utils/fileManagement.js';
import { shouldCrashDev } from '../../utils/development.js'; import { shouldCrashDev } from '../../utils/development.js';
-8
View File
@@ -1,8 +0,0 @@
// resolve running environment
const env = process.env.NODE_ENV || 'production';
export const isTest = Boolean(process.env.IS_TEST);
export const environment = isTest ? 'test' : env;
export const isDocker = env === 'docker';
export const isProduction = isDocker || (env === 'production' && !isTest);
export const isOntimeCloud = Boolean(process.env.IS_CLOUD);
+1 -1
View File
@@ -10,7 +10,7 @@ import { dirname, join } from 'path';
import { config } from './config.js'; import { config } from './config.js';
import { ensureDirectory } from '../utils/fileManagement.js'; import { ensureDirectory } from '../utils/fileManagement.js';
import { isProduction } from './environment.js'; import { isProduction } from '../externals.js';
/** /**
* Returns public path depending on OS * Returns public path depending on OS
+1 -1
View File
@@ -1,4 +1,4 @@
import { isProduction } from '../setup/environment.js'; import { isProduction } from '../externals.js';
import { consoleError } from './console.js'; import { consoleError } from './console.js';
+1 -1
View File
@@ -4,7 +4,7 @@ import type { Server } from 'http';
import { networkInterfaces } from 'os'; import { networkInterfaces } from 'os';
import type { AddressInfo } from 'net'; import type { AddressInfo } from 'net';
import { isDocker, isOntimeCloud, isProduction } from '../setup/environment.js'; import { isDocker, isOntimeCloud, isProduction } from '../externals.js';
import { logger } from '../classes/Logger.js'; import { logger } from '../classes/Logger.js';
/** /**
+1 -1
View File
@@ -215,7 +215,7 @@ export const parseExcel = (
const maybeTimeType = makeString(column, ''); const maybeTimeType = makeString(column, '');
if (maybeTimeType === 'block') { if (maybeTimeType === 'block') {
event.type = SupportedEvent.Block; event.type = SupportedEvent.Block;
} else if (maybeTimeType === '' || maybeTimeType === 'event' || isKnownTimerType(maybeTimeType)) { } else if (maybeTimeType === '' || isKnownTimerType(maybeTimeType)) {
event.type = SupportedEvent.Event; event.type = SupportedEvent.Event;
event.timerType = validateTimerType(maybeTimeType); event.timerType = validateTimerType(maybeTimeType);
} else { } else {
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "3.16.1", "version": "3.16.1-alpha.2",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"keywords": [ "keywords": [
"ontime", "ontime",
@@ -16,8 +16,8 @@
}, },
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"engines": { "engines": {
"node": "~22", "node": "~20",
"pnpm": "~10" "pnpm": "~9"
}, },
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -50,5 +50,5 @@
"turbo": "^2.3.3", "turbo": "^2.3.3",
"typescript": "catalog:" "typescript": "catalog:"
}, },
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977" "packageManager": "pnpm@9.15.0"
} }
@@ -15,7 +15,6 @@ export interface SessionStats {
projectName: string; projectName: string;
playback: Playback; playback: Playback;
timezone: string; timezone: string;
version: string;
} }
export interface GetInfo { export interface GetInfo {
+627 -1823
View File
File diff suppressed because it is too large Load Diff
+7 -9
View File
@@ -1,16 +1,14 @@
# https://pnpm.io/pnpm-workspace_yaml
packages: packages:
- apps/* - "apps/*"
- packages/* - "packages/*"
catalog: catalog:
typescript: 5.5.3 typescript: 5.5.3
'@typescript-eslint/eslint-plugin': 7.16.1 "@typescript-eslint/eslint-plugin": 7.16.1
'@typescript-eslint/parser': 7.16.1 "@typescript-eslint/parser": 7.16.1
'@types/node': 22.15.26 "@types/node": 20.17.16
prettier: 3.3.1 prettier: 3.3.1
eslint: 8.56.0 eslint: 8.56.0
eslint-config-prettier: 9.1.0 eslint-config-prettier: 9.1.0
eslint-plugin-prettier: 5.1.3 eslint-plugin-prettier: 5.1.3
vitest: 3.2.1 vitest: 2.1.6
onlyBuiltDependencies:
- electron
- esbuild