From cc34db775a78e52877564bea34e4171232248fa3 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Mon, 8 Sep 2025 06:51:07 +0200 Subject: [PATCH 01/17] chore: bump version --- apps/cli/package.json | 2 +- apps/client/package.json | 2 +- apps/electron/package.json | 2 +- apps/server/package.json | 2 +- package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 62c180759b..34954faea7 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@getontime/cli", - "version": "4.0.0-beta.2", + "version": "4.0.0-beta.3", "author": "Carlos Valente", "description": "Time keeping for live events", "repository": "https://github.com/cpvalente/ontime", diff --git a/apps/client/package.json b/apps/client/package.json index 33397fe508..e123da8136 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,6 +1,6 @@ { "name": "ontime-ui", - "version": "4.0.0-beta.2", + "version": "4.0.0-beta.3", "private": true, "type": "module", "dependencies": { diff --git a/apps/electron/package.json b/apps/electron/package.json index 7b6f971289..54fe0661cd 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -1,6 +1,6 @@ { "name": "ontime-electron", - "version": "4.0.0-beta.2", + "version": "4.0.0-beta.3", "author": "Carlos Valente", "description": "Time keeping for live events", "repository": "https://github.com/cpvalente/ontime", diff --git a/apps/server/package.json b/apps/server/package.json index 9edfcd609c..71716d9d24 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -2,7 +2,7 @@ "name": "ontime-server", "type": "module", "main": "src/index.ts", - "version": "4.0.0-beta.2", + "version": "4.0.0-beta.3", "exports": "./src/index.js", "dependencies": { "@googleapis/sheets": "^5.0.5", diff --git a/package.json b/package.json index 4c0d37e34b..aaa674957e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ontime", - "version": "4.0.0-beta.2", + "version": "4.0.0-beta.3", "description": "Time keeping for live events", "keywords": [ "ontime", From 2e950965e0efe602796da94f004a11db2132ebc5 Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Mon, 8 Sep 2025 09:49:01 +0200 Subject: [PATCH 02/17] fix: sheet default import map (#1775) * fix: default import map * fix: update test --- apps/server/src/api-data/excel/__tests__/mockData.ts | 2 +- .../utils/src/feature/spreadsheet-import/spreadsheetImport.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/api-data/excel/__tests__/mockData.ts b/apps/server/src/api-data/excel/__tests__/mockData.ts index f61ba22a7f..16a9ebe21a 100644 --- a/apps/server/src/api-data/excel/__tests__/mockData.ts +++ b/apps/server/src/api-data/excel/__tests__/mockData.ts @@ -10,7 +10,7 @@ export const dataFromExcelTemplate = [ 'Timer type', 'Count to end', 'Skip', - 'Notes', + 'Note', 't0', 'Test1', 'test2', diff --git a/packages/utils/src/feature/spreadsheet-import/spreadsheetImport.ts b/packages/utils/src/feature/spreadsheet-import/spreadsheetImport.ts index acee65134a..b227e89c8b 100644 --- a/packages/utils/src/feature/spreadsheet-import/spreadsheetImport.ts +++ b/packages/utils/src/feature/spreadsheet-import/spreadsheetImport.ts @@ -14,7 +14,7 @@ export const defaultImportMap = { title: 'title', countToEnd: 'count to end', skip: 'skip', - note: 'notes', + note: 'note', colour: 'colour', endAction: 'end action', timerType: 'timer type', From 490e429f446f8a67bbace8fcb34b0897414c1ad7 Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Sat, 13 Sep 2025 16:31:56 +0200 Subject: [PATCH 03/17] cleanup (#1776) * chore: cleanup leftover console log * chore: prevent error in client tsconfig --- apps/client/src/common/hooks/useSocket.ts | 1 - apps/client/tsconfig.json | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 8e6a6e142a..254f6a814f 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -11,7 +11,6 @@ const createSelector = export const setClientRemote = { setIdentify: (payload: { target: string; identify: boolean }) => sendSocket('client', payload), setRedirect: (payload: { target: string; redirect: string }) => { - console.log('--- got', payload); sendSocket('client', payload); }, setClientName: (payload: { target: string; rename: string }) => sendSocket('client', payload), diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json index fbf953dd1f..5629eaa955 100644 --- a/apps/client/tsconfig.json +++ b/apps/client/tsconfig.json @@ -32,7 +32,8 @@ "preserveConstEnums": true, "allowJs": true, "baseUrl": "src", - "jsx": "react-jsx" + "jsx": "react-jsx", + "outDir": "build" }, "include": [ "src" From bfbf8574e322d62d8b37624470417e77d01f863c Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 9 Sep 2025 07:39:38 +0200 Subject: [PATCH 04/17] fix: phase style overrides in timer --- apps/client/src/common/models/View.types.ts | 19 ------------------- apps/client/src/views/timer/Timer.tsx | 6 +++--- apps/client/src/views/timer/timer.options.ts | 10 +++++----- apps/server/src/models/demoProject.ts | 2 +- 4 files changed, 9 insertions(+), 28 deletions(-) delete mode 100644 apps/client/src/common/models/View.types.ts diff --git a/apps/client/src/common/models/View.types.ts b/apps/client/src/common/models/View.types.ts deleted file mode 100644 index 2d8979a666..0000000000 --- a/apps/client/src/common/models/View.types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type OverridableOptions = { - keyColour?: string; - textColour?: string; - textBackground?: string; - font?: string; - size?: number; - justifyContent?: 'start' | 'center' | 'end'; - alignItems?: 'start' | 'center' | 'end'; - left?: string; - top?: string; - hideNav?: boolean; - hideOvertime?: boolean; - hideMessagesOverlay?: boolean; - hideEndMessage?: boolean; - language?: string; - showProgressBar?: boolean; - hideTimerSeconds?: boolean; - removeLeadingZeros?: boolean; -}; diff --git a/apps/client/src/views/timer/Timer.tsx b/apps/client/src/views/timer/Timer.tsx index d9a9e659e3..187052d01c 100644 --- a/apps/client/src/views/timer/Timer.tsx +++ b/apps/client/src/views/timer/Timer.tsx @@ -68,7 +68,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings } hidePhase, font, keyColour, - textColour, + timerColour, } = useTimerOptions(); const { getLocalizedString } = useTranslation(); @@ -131,11 +131,11 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings } ); // gather presentation styles - const timerColour = getTimerColour(viewSettings, textColour, showWarning, showDanger); + const resolvedTimerColour = getTimerColour(viewSettings, timerColour, showWarning, showDanger); const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, secondaryContent); const userStyles = { ...(keyColour && { '--timer-bg': keyColour }), - ...(textColour && { '--timer-colour': timerColour }), + ...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }), ...(font && { '--timer-font': font }), }; diff --git a/apps/client/src/views/timer/timer.options.ts b/apps/client/src/views/timer/timer.options.ts index 4d88636b9e..ca7a3238b7 100644 --- a/apps/client/src/views/timer/timer.options.ts +++ b/apps/client/src/views/timer/timer.options.ts @@ -163,9 +163,9 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields): defaultValue: '101010', }, { - id: 'textColour', - title: 'Text Colour', - description: 'Text colour. Default: #f6f6f6', + id: 'timerColour', + title: 'Timer Colour', + description: 'Timer colour. Default: #f6f6f6', type: 'colour', defaultValue: 'f6f6f6', }, @@ -191,7 +191,7 @@ type TimerOptions = { hidePhase: boolean; font?: string; keyColour?: string; - textColour?: string; + timerColour?: string; }; /** @@ -226,7 +226,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL font: getValue('font') ?? undefined, keyColour: makeColourString(getValue('keyColour')), - textColour: makeColourString(getValue('textColour')), + timerColour: makeColourString(getValue('timerColour')), }; } diff --git a/apps/server/src/models/demoProject.ts b/apps/server/src/models/demoProject.ts index c0d5b9da4c..9aaee14634 100644 --- a/apps/server/src/models/demoProject.ts +++ b/apps/server/src/models/demoProject.ts @@ -337,7 +337,7 @@ export const demoDb: DatabaseModel = { alias: 'minimal', target: OntimeView.Timer, search: - 'hideclock=true&hidecards=true&hideprogress=true&hidemessage=true&hidesecondary=true&hidelogo=true&font=arial+black&keycolour=00ff00&textcolour=ffffff', + 'hideclock=true&hidecards=true&hideprogress=true&hidemessage=true&hidesecondary=true&hidelogo=true&font=arial+black&keycolour=00ff00&timerColour=ffffff', }, ], customFields: { From 5810ae0d543df87c7196bfc828de022785fa2c80 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 9 Sep 2025 15:55:18 +0200 Subject: [PATCH 05/17] refactor: add default value to secondary source --- apps/client/src/views/timer/timer.options.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/client/src/views/timer/timer.options.ts b/apps/client/src/views/timer/timer.options.ts index ca7a3238b7..e44d311b48 100644 --- a/apps/client/src/views/timer/timer.options.ts +++ b/apps/client/src/views/timer/timer.options.ts @@ -25,10 +25,12 @@ const timerDisplayOptions: SelectOption[] = [ export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { const mainOptions = makeOptionsFromCustomFields(customFields, [ + { value: 'none', label: 'None' }, { value: 'title', label: 'Title' }, { value: 'note', label: 'Note' }, ]); const secondaryOptions = makeOptionsFromCustomFields(customFields, [ + { value: 'none', label: 'None' }, { value: 'title', label: 'Title' }, { value: 'note', label: 'Note' }, ]); @@ -84,7 +86,7 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields): description: 'Select the data source for the main text', type: 'option', values: mainOptions, - defaultValue: 'Title', + defaultValue: 'title', }, { id: 'secondary-src', @@ -92,7 +94,7 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields): description: 'Select the data source for the secondary text', type: 'option', values: secondaryOptions, - defaultValue: '', + defaultValue: 'none', }, ], }, From c79ee193c940e1c11822e173ad22b1b2f7136181 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 9 Sep 2025 15:55:30 +0200 Subject: [PATCH 06/17] fix: prevent layout reflow on different entry types --- .../src/features/rundown/RundownExport.module.scss | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/client/src/features/rundown/RundownExport.module.scss b/apps/client/src/features/rundown/RundownExport.module.scss index 1e262b1647..45ddbd32c8 100644 --- a/apps/client/src/features/rundown/RundownExport.module.scss +++ b/apps/client/src/features/rundown/RundownExport.module.scss @@ -28,20 +28,22 @@ padding-inline: 0; box-shadow: $box-shadow-right; - flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */ + flex: 1 1 0; /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */ min-width: 38rem; - max-width: 60rem; + max-width: none; + width: 0; } .side { max-height: 100%; - max-width: 45rem; margin: 0.5rem 0; padding: 1rem; padding-right: 0; background-color: $gray-1325; border-radius: 0 8px 8px 0; - flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */ - max-width: 45rem; + flex: 1 1 0; /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */ + // width is locked to swatch picker elements + min-width: calc(15 * 2rem + 13 * 0.5rem); + width: 0; } From 578cb2b2443c824d363fe2c9e03ad085dd6a49bb Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 9 Sep 2025 15:55:40 +0200 Subject: [PATCH 07/17] refactor: improve pin styling --- .../src/common/components/protect-route/PinPage.module.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/client/src/common/components/protect-route/PinPage.module.scss b/apps/client/src/common/components/protect-route/PinPage.module.scss index 2af602fa2b..073397cbc3 100644 --- a/apps/client/src/common/components/protect-route/PinPage.module.scss +++ b/apps/client/src/common/components/protect-route/PinPage.module.scss @@ -11,9 +11,11 @@ } .pin { + margin-top: 0.5rem; display: flex; align-items: center; justify-content: center; + gap: 4px; input { font-size: 4rem; From 8d3ce46c563c6c3792d2e2923ddacfacef17af8e Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 9 Sep 2025 15:59:39 +0200 Subject: [PATCH 08/17] fix: allow moving a group after another --- apps/client/src/features/rundown/rundown.utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/features/rundown/rundown.utils.ts b/apps/client/src/features/rundown/rundown.utils.ts index e9f0076326..43a00675b6 100644 --- a/apps/client/src/features/rundown/rundown.utils.ts +++ b/apps/client/src/features/rundown/rundown.utils.ts @@ -41,9 +41,9 @@ export function canDrop( order?: 'after' | 'before', isTargetCollapsed?: boolean, ): boolean { - // this would mean inserting a group inside another + // inserting before would mean adding a group inside another if (targetType === 'end-group') { - return false; + return order === 'after'; } // this means swapping places with another group From ded8bddb2d3a4726eba4054403ac8eb763eceb1a Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 9 Sep 2025 20:24:55 +0200 Subject: [PATCH 09/17] refactor: improve automated following --- .../src/common/hooks/useFollowComponent.ts | 75 ++++--------------- .../client/src/features/operator/Operator.tsx | 1 + apps/client/src/features/rundown/Rundown.tsx | 9 ++- .../countdown/CountdownSubscriptions.tsx | 1 + 4 files changed, 25 insertions(+), 61 deletions(-) diff --git a/apps/client/src/common/hooks/useFollowComponent.ts b/apps/client/src/common/hooks/useFollowComponent.ts index 60f187f877..dafdcdc581 100644 --- a/apps/client/src/common/hooks/useFollowComponent.ts +++ b/apps/client/src/common/hooks/useFollowComponent.ts @@ -1,6 +1,5 @@ -import { RefObject, useCallback, useEffect, useRef } from 'react'; - -import { useSelectedEventId } from './useSocket'; +import { RefObject, useCallback, useEffect } from 'react'; +import { MaybeString } from 'ontime-types'; function scrollToComponent( componentRef: RefObject, @@ -18,37 +17,26 @@ function scrollToComponent( - componentRef: RefObject, - scrollRef: RefObject, - topOffset: number, -) { - if (!componentRef.current || !scrollRef.current) { - return; - } - - const componentRect = componentRef.current.getBoundingClientRect(); - const scrollRect = scrollRef.current.getBoundingClientRect(); - const top = componentRect.top - scrollRect.top + scrollRef.current.scrollTop - topOffset; - - // maintain current x scroll position - scrollRef.current.scrollTo(scrollRef.current.scrollLeft, top); -} - interface UseFollowComponentProps { followRef: RefObject; scrollRef: RefObject; doFollow: boolean; topOffset?: number; setScrollFlag?: (newValue: boolean) => void; + followTrigger?: MaybeString; // this would be an entry id or null } -export default function useFollowComponent(props: UseFollowComponentProps) { - const { followRef, scrollRef, doFollow, topOffset = 100, setScrollFlag } = props; - - // when cursor moves, view should follow +export default function useFollowComponent({ + followRef, + scrollRef, + doFollow, + topOffset = 100, + setScrollFlag, + followTrigger, +}: UseFollowComponentProps) { + // when trigger moves, view should follow useEffect(() => { - if (!doFollow) { + if (!doFollow || !followTrigger) { return; } @@ -60,16 +48,14 @@ export default function useFollowComponent(props: UseFollowComponentProps) { setScrollFlag?.(false); }); } - - // eslint-disable-next-line -- the prompt seems incorrect - }, [followRef?.current, scrollRef?.current]); + }, [followTrigger, doFollow, followRef, scrollRef, setScrollFlag, topOffset]); const scrollToRefComponent = useCallback( (componentRef = followRef, containerRef = scrollRef, offset = topOffset) => { - if (componentRef.current && containerRef.current) { + if (componentRef && containerRef) { // @ts-expect-error -- we know this are not null // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - scrollToComponent(componentRef!, scrollRef!, offset); + scrollToComponent(componentRef!, containerRef!, offset); } }, [followRef, scrollRef, topOffset], @@ -77,32 +63,3 @@ export default function useFollowComponent(props: UseFollowComponentProps) { return scrollToRefComponent; } - -export function useFollowSelected(doFollow: boolean, topOffset = 100) { - const selectedEvenId = useSelectedEventId(); - - const selectedRef = useRef(null); - const scrollRef = useRef(null); - - useEffect(() => { - if (!doFollow) { - return; - } - - if (selectedEvenId && selectedRef.current && scrollRef.current) { - // Use requestAnimationFrame to ensure the component is fully loaded - window.requestAnimationFrame(() => { - snapToComponent( - { current: selectedRef.current } as RefObject, - { current: scrollRef.current } as RefObject, - topOffset, - ); - }); - } - }, [doFollow, selectedEvenId, topOffset]); - - return { - selectedRef, - scrollRef, - }; -} diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index 22406b07c7..887f8027e9 100644 --- a/apps/client/src/features/operator/Operator.tsx +++ b/apps/client/src/features/operator/Operator.tsx @@ -49,6 +49,7 @@ export default function Operator() { scrollRef, doFollow: !lockAutoScroll, topOffset: selectedOffset, + followTrigger: selectedEventId, }); useWindowTitle('Operator'); diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 3c60f2cf39..1075b4c01e 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -82,7 +82,12 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) { const cursorRef = useRef(null); const scrollRef = useRef(null); - useFollowComponent({ followRef: cursorRef, scrollRef, doFollow: editorMode === AppMode.Run }); + useFollowComponent({ + followRef: cursorRef, + scrollRef, + doFollow: true, + followTrigger: editorMode === AppMode.Edit ? cursor : featureData?.selectedEventId, + }); // DND KIT const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 10 } })); @@ -312,7 +317,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) { setMetadata(rundownMetadata); }, [order, entries, rundownMetadata]); - // in run mode, we follow selection + // in run mode, we follow the playback selection and open groups as needed useEffect(() => { if (editorMode !== AppMode.Run || !featureData?.selectedEventId) { return; diff --git a/apps/client/src/views/countdown/CountdownSubscriptions.tsx b/apps/client/src/views/countdown/CountdownSubscriptions.tsx index b14f1041a0..d51ea298c3 100644 --- a/apps/client/src/views/countdown/CountdownSubscriptions.tsx +++ b/apps/client/src/views/countdown/CountdownSubscriptions.tsx @@ -53,6 +53,7 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode scrollRef, doFollow: !lockAutoScroll, topOffset: 0, + followTrigger: selectedEventId, }); // reset scroll if nothing is selected From e4b0df42cf54d66d9a9a9d4a2ef8fe61b020817f Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 9 Sep 2025 20:25:25 +0200 Subject: [PATCH 10/17] feat: allow finding milestones --- .../views/editor/finder/Finder.module.scss | 1 + .../client/src/views/editor/finder/Finder.tsx | 7 +- .../src/views/editor/finder/useFinder.tsx | 66 +++++++++++++------ 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/apps/client/src/views/editor/finder/Finder.module.scss b/apps/client/src/views/editor/finder/Finder.module.scss index 32114c26b8..9b7032bd2d 100644 --- a/apps/client/src/views/editor/finder/Finder.module.scss +++ b/apps/client/src/views/editor/finder/Finder.module.scss @@ -66,4 +66,5 @@ .scrollContainer { max-height: 70vh; overflow: auto; + padding-top: 1rem; } \ No newline at end of file diff --git a/apps/client/src/views/editor/finder/Finder.tsx b/apps/client/src/views/editor/finder/Finder.tsx index 3d39fbe965..65244945bb 100644 --- a/apps/client/src/views/editor/finder/Finder.tsx +++ b/apps/client/src/views/editor/finder/Finder.tsx @@ -71,8 +71,7 @@ export default function Finder({ isOpen, onClose }: FinderProps) { results.map((entry, index) => { const isSelected = selected === index; const displayIndex = entry.type === SupportedEntry.Event ? entry.eventIndex : '-'; - const displayCue = entry.type === SupportedEntry.Event ? entry.cue : ''; - const colour = entry.type === SupportedEntry.Event ? entry.colour : ''; + const displayCue = 'cue' in entry ? entry.cue : ''; return (
  • -
    +
    {displayIndex}
    {displayCue}
    @@ -99,7 +98,7 @@ export default function Finder({ isOpen, onClose }: FinderProps) { footerElements={
    Use the keywords cue, index or - title to filter search + title to filter search.
    } /> diff --git a/apps/client/src/views/editor/finder/useFinder.tsx b/apps/client/src/views/editor/finder/useFinder.tsx index 65ea5a7574..764e4b656d 100644 --- a/apps/client/src/views/editor/finder/useFinder.tsx +++ b/apps/client/src/views/editor/finder/useFinder.tsx @@ -1,6 +1,6 @@ import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react'; import { useSessionStorage } from '@mantine/hooks'; -import { EntryId, isOntimeEvent, isOntimeGroup, MaybeString, SupportedEntry } from 'ontime-types'; +import { EntryId, isOntimeEvent, isOntimeGroup, isOntimeMilestone, MaybeString, SupportedEntry } from 'ontime-types'; import { useFlatRundown } from '../../../common/hooks-query/useRundown'; import { useEventSelection } from '../../../features/rundown/useEventSelection'; @@ -9,14 +9,15 @@ const maxResults = 12; type FilterableGroup = { type: SupportedEntry.Group; - id: string; + id: EntryId; index: number; title: string; + colour: string; }; type FilterableEvent = { type: SupportedEntry.Event; - id: string; + id: EntryId; index: number; eventIndex: number; title: string; @@ -25,7 +26,17 @@ type FilterableEvent = { parent: MaybeString; }; -type FilterableEntry = FilterableGroup | FilterableEvent; +type FilterableMilestone = { + type: SupportedEntry.Milestone; + id: EntryId; + index: number; + title: string; + cue: string; + colour: string; + parent: MaybeString; +}; + +type FilterableEntry = FilterableGroup | FilterableEvent | FilterableMilestone; export default function useFinder() { const { data, rundownId } = useFlatRundown(); @@ -59,7 +70,7 @@ export default function useFinder() { lastSearchString.current = searchValue; if (searchValue.startsWith('index ')) { - const searchString = searchValue.replace('index ', '').trim(); + const searchString = searchValue.slice('index '.length).trim(); const { results, error } = searchByIndex(searchString); setResults(results); setError(error); @@ -67,14 +78,14 @@ export default function useFinder() { } if (searchValue.startsWith('cue ')) { - const searchString = searchValue.replace('cue ', '').trim(); + const searchString = searchValue.slice('cue '.length).trim(); const { results, error } = searchByCue(searchString); setResults(results); setError(error); return; } - const searchString = searchValue.replace('title ', '').trim(); + const searchString = searchValue.startsWith('title ') ? searchValue.slice('title '.length).trim() : searchValue; const { results, error } = searchByTitle(searchString); setResults(results); setError(error); @@ -162,33 +173,46 @@ export default function useFinder() { break; } - const event = data[i]; - if (isOntimeEvent(event)) { - if (event.title.toLowerCase().includes(searchString)) { + const entry = data[i]; + if (isOntimeEvent(entry)) { + if (entry.title.toLowerCase().includes(searchString)) { remaining--; results.push({ type: SupportedEntry.Event, - id: event.id, + id: entry.id, index: i, eventIndex, - title: event.title, - cue: event.cue, - colour: event.colour, - parent: event.parent, + title: entry.title, + cue: entry.cue, + colour: entry.colour, + parent: entry.parent, } satisfies FilterableEvent); } eventIndex++; - } - if (isOntimeGroup(event)) { - if (event.title.toLowerCase().includes(searchString)) { + } else if (isOntimeGroup(entry)) { + if (entry.title.toLowerCase().includes(searchString)) { remaining--; results.push({ type: SupportedEntry.Group, - id: event.id, + id: entry.id, index: i, - title: event.title, + title: entry.title, + colour: entry.colour, } satisfies FilterableGroup); } + } else if (isOntimeMilestone(entry)) { + if (entry.title.toLowerCase().includes(searchString)) { + remaining--; + results.push({ + type: SupportedEntry.Milestone, + id: entry.id, + index: i, + title: entry.title, + cue: entry.cue, + colour: entry.colour, + parent: entry.parent, + } satisfies FilterableMilestone); + } } } return { results, error: null }; @@ -200,7 +224,7 @@ export default function useFinder() { const select = useCallback( (selectedEvent: FilterableEntry) => { // First expand the parent group if this is an event inside a group - if (selectedEvent.type === SupportedEntry.Event && selectedEvent.parent !== null) { + if ('parent' in selectedEvent && selectedEvent.parent !== null) { // Try direct state update instead of using callback const currentGroups = [...new Set(collapsedGroups)]; const newGroups = currentGroups.filter((id) => id !== selectedEvent.parent); From f6a02abf3614c5fccab93ba0a49e2e22c63ebec6 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Wed, 10 Sep 2025 16:15:08 +0200 Subject: [PATCH 11/17] fix: skip nested events --- apps/client/src/features/operator/Operator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index 887f8027e9..db766224de 100644 --- a/apps/client/src/features/operator/Operator.tsx +++ b/apps/client/src/features/operator/Operator.tsx @@ -182,7 +182,7 @@ export default function Operator() { const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEntry); // hide past events (if setting) and skipped events - if (hidePast && isPast) { + if ((hidePast && isPast) || nestedEntry.skip) { return null; } From 7cd92ce5f7c175ca6f74cfcc6c27caa2327f7045 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 14 Sep 2025 10:16:13 +0200 Subject: [PATCH 12/17] docs: add contribution guidelines --- DEVELOPMENT.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 98c60bcea2..130d301c73 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -99,3 +99,16 @@ Other useful commands - __List running processes__ by running `docker ps` - __Kill running process__ by running `docker kill ` + +## CONTRIBUTION GUIDELINES + +If you want to propose changes to the codebase, please reach out before opening a Pull Request. + +For new PRs, please follow the following checklist: +* [ ] You have updated and ran unit locally and they are passing. Unit tests are generally created for all utility functions and business logic +* [ ] You have ran code formatting and linting in all your changes +* [ ] The branch is clean and the commits are meaningfully separated and contain descriptive messages +* [ ] The PR body contains description and motivation for the changes + +After this checklist is complete, you can request a review from one of the maintainers to get feedback and approval on the changes. \ +We will review as soon as possible From b51e7cbd2ded3fc28368aa989d28a4c51cc4afb4 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 14 Sep 2025 10:16:31 +0200 Subject: [PATCH 13/17] fix: maintain multiline in cuesheet cells --- .../cuesheet-table-elements/GhostedText.module.scss | 4 ++++ .../cuesheet-table-elements/GhostedText.tsx | 8 ++++++-- .../cuesheet-table-elements/cuesheetColsFactory.tsx | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GhostedText.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GhostedText.module.scss index 905f61c919..8868aa68d2 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GhostedText.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GhostedText.module.scss @@ -4,3 +4,7 @@ padding-top: 0.25em; width: 100%; } + +.multiline { + white-space: break-spaces; +} diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GhostedText.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GhostedText.tsx index 56f3bcf352..ba9a852f80 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GhostedText.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GhostedText.tsx @@ -2,6 +2,10 @@ import { PropsWithChildren } from 'react'; import style from './GhostedText.module.scss'; -export default function GhostedText({ children }: PropsWithChildren) { - return
    {children}
    ; +interface GhostedTextProps { + multiline?: boolean; +} + +export default function GhostedText({ children, multiline }: PropsWithChildren) { + return
    {children}
    ; } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx index c63114a29c..1125c81bfa 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx @@ -143,7 +143,7 @@ function MakeMultiLineField({ row, column, table }: CellContext{initialValue}; + return {initialValue}; } return ; From 3e8a6dc4e53fbc8b1c252f3225f0c5cf8daa9b63 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Fri, 5 Sep 2025 18:45:06 +0200 Subject: [PATCH 14/17] feat: send refetch for css override --- apps/client/src/common/api/constants.ts | 1 + apps/client/src/common/utils/socket.ts | 4 ++++ apps/server/src/api-data/assets/assets.service.ts | 4 +++- packages/types/src/api/websocket/refetch.type.ts | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/client/src/common/api/constants.ts b/apps/client/src/common/api/constants.ts index aad81975f5..731585209e 100644 --- a/apps/client/src/common/api/constants.ts +++ b/apps/client/src/common/api/constants.ts @@ -16,6 +16,7 @@ export const VIEW_SETTINGS = ['viewSettings']; export const CLIENT_LIST = ['clientList']; export const REPORT = ['report']; export const TRANSLATION = ['translation']; +export const CSS_OVERRIDE = ['cssOverride'] // API URLs export const apiEntryUrl = `${serverURL}/data`; diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index df8fc5b677..ddd56584ff 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -13,6 +13,7 @@ import { isProduction, websocketUrl } from '../../externals'; import { APP_SETTINGS, CLIENT_LIST, + CSS_OVERRIDE, CUSTOM_FIELDS, PROJECT_DATA, REPORT, @@ -181,6 +182,9 @@ export const connectSocket = () => { case RefetchKey.Settings: ontimeQueryClient.invalidateQueries({ queryKey: APP_SETTINGS }); break; + case RefetchKey.CssOverride: + ontimeQueryClient.invalidateQueries({ queryKey: CSS_OVERRIDE }); + break; default: { target satisfies never; break; diff --git a/apps/server/src/api-data/assets/assets.service.ts b/apps/server/src/api-data/assets/assets.service.ts index 4e208a2ca1..094481f2aa 100644 --- a/apps/server/src/api-data/assets/assets.service.ts +++ b/apps/server/src/api-data/assets/assets.service.ts @@ -1,10 +1,11 @@ -import type { TranslationObject } from 'ontime-types'; +import { RefetchKey, type TranslationObject } from 'ontime-types'; import { existsSync } from 'node:fs'; import { readFile, writeFile } from 'node:fs/promises'; import { publicFiles } from '../../setup/index.js'; import { defaultCss } from '../../user/styles/bundledCss.js'; +import { sendRefetch } from '../../adapters/WebsocketAdapter.js'; /** * Reads the user's css file @@ -33,6 +34,7 @@ export async function writeCssFile(css: string) { } await writeFile(path, css, { encoding: 'utf8' }); + sendRefetch(RefetchKey.CssOverride); } /** diff --git a/packages/types/src/api/websocket/refetch.type.ts b/packages/types/src/api/websocket/refetch.type.ts index fdaac6b751..8d13765775 100644 --- a/packages/types/src/api/websocket/refetch.type.ts +++ b/packages/types/src/api/websocket/refetch.type.ts @@ -8,4 +8,5 @@ export enum RefetchKey { ViewSettings = 'view-settings', Translation = 'translation', Settings = 'settings', + CssOverride = 'css', } From 294a8164093c160ad7729411c0b99684cfb7c94d Mon Sep 17 00:00:00 2001 From: arc-alex Date: Fri, 5 Sep 2025 19:06:35 +0200 Subject: [PATCH 15/17] refactor: css fetch logic --- apps/client/src/common/api/constants.ts | 2 - .../src/common/hooks/useRuntimeStylesheet.ts | 80 +++++++------------ apps/client/src/views/ViewLoader.tsx | 5 +- 3 files changed, 30 insertions(+), 57 deletions(-) diff --git a/apps/client/src/common/api/constants.ts b/apps/client/src/common/api/constants.ts index 731585209e..7b2913c75f 100644 --- a/apps/client/src/common/api/constants.ts +++ b/apps/client/src/common/api/constants.ts @@ -22,9 +22,7 @@ export const CSS_OVERRIDE = ['cssOverride'] export const apiEntryUrl = `${serverURL}/data`; const userAssetsPath = 'user'; -const cssOverridePath = 'styles/override.css'; const customTranslationsPath = 'translations/translations.json'; -export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`; export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`; export const customTranslationsURL = `${serverURL}/${userAssetsPath}/${customTranslationsPath}`; diff --git a/apps/client/src/common/hooks/useRuntimeStylesheet.ts b/apps/client/src/common/hooks/useRuntimeStylesheet.ts index c757b71171..b43c95b4dc 100644 --- a/apps/client/src/common/hooks/useRuntimeStylesheet.ts +++ b/apps/client/src/common/hooks/useRuntimeStylesheet.ts @@ -1,9 +1,24 @@ import { useEffect, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { MILLIS_PER_HOUR } from 'ontime-utils'; + +import { getCSSContents } from '../api/assets'; +import { CSS_OVERRIDE } from '../api/constants'; +import useViewSettings from '../hooks-query/useViewSettings'; const scriptTagId = 'ontime-override'; -export const useRuntimeStylesheet = (pathToFile?: string): { shouldRender: boolean } => { +export const useRuntimeStylesheet = (): { shouldRender: boolean } => { const [shouldRender, setShouldRender] = useState(false); + const { data } = useViewSettings(); + + const { data: cssData } = useQuery({ + queryKey: CSS_OVERRIDE, + queryFn: getCSSContents, + enabled: data.overrideStyles, + placeholderData: (previousData, _previousQuery) => previousData, + staleTime: MILLIS_PER_HOUR, + }); /** * When a view mounts or the stylesheet path changes we need to handle potentially loading a new stylesheet @@ -12,64 +27,27 @@ export const useRuntimeStylesheet = (pathToFile?: string): { shouldRender: boole * @returns { shouldRender: boolean } - after the stylesheet is handled and the clients are ready to render */ useEffect(() => { - if (!pathToFile) { - handleNoStylesheet(); - return; - } - - // there is already a stylesheet loaded, nothing further to do - if (document.getElementById(scriptTagId)) { + if (!cssData || !data.overrideStyles) { + /** + * No stylesheet was provided, remove any existing stylesheet + */ + document.getElementById(scriptTagId)?.remove(); setShouldRender(true); return; } setShouldRender(false); - - fetchStylesheetData(pathToFile) - .then((data: string | undefined) => { - if (!data) { - console.error('Error loading stylesheet: no data'); - return; - } - return injectStylesheet(data); - }) - .catch((error: unknown) => { - console.error(`Error loading stylesheet: ${error}`); - }) - .finally(() => { - // schedule render for next tick - setTimeout(() => setShouldRender(true), 0); - }); - - /** - * No stylesheet was provided, remove any existing stylesheet - */ - function handleNoStylesheet() { - document.getElementById(scriptTagId)?.remove(); - setShouldRender(true); - } - - /** - * Get data from backend - */ - async function fetchStylesheetData(path: string) { - const response = await fetch(path); - if (response.ok) { - return response.text(); - } - return undefined; - } - /** * Add a stylesheet with given content to the document head */ - async function injectStylesheet(styleContent: string) { - const styleSheet = document.createElement('style'); - styleSheet.setAttribute('id', scriptTagId); - styleSheet.innerHTML = styleContent; - document.head.append(styleSheet); - } - }, [pathToFile]); + const styleSheet = document.createElement('style'); + styleSheet.setAttribute('id', scriptTagId); + styleSheet.innerHTML = cssData; + document.head.append(styleSheet); + + // schedule render for next tick + setTimeout(() => setShouldRender(true), 0); + }, [cssData, data.overrideStyles]); return { shouldRender }; }; diff --git a/apps/client/src/views/ViewLoader.tsx b/apps/client/src/views/ViewLoader.tsx index ed5df9db44..b7cb1b3146 100644 --- a/apps/client/src/views/ViewLoader.tsx +++ b/apps/client/src/views/ViewLoader.tsx @@ -1,14 +1,11 @@ import { PropsWithChildren } from 'react'; -import { overrideStylesURL } from '../common/api/constants'; import { useRuntimeStylesheet } from '../common/hooks/useRuntimeStylesheet'; -import useViewSettings from '../common/hooks-query/useViewSettings'; import Loader from './common/loader/Loader'; export default function ViewLoader({ children }: PropsWithChildren) { - const { data } = useViewSettings(); - const { shouldRender } = useRuntimeStylesheet(data.overrideStyles ? overrideStylesURL : undefined); + const { shouldRender } = useRuntimeStylesheet(); // eventually we would want to leverage suspense here // while the feature is not ready, we simply trigger a loader From 3c874aae256ce62b1561b353de0626312c9b6e7f Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sun, 7 Sep 2025 13:33:17 +0200 Subject: [PATCH 16/17] refactor: cleanup a bit --- .../src/common/hooks/useRuntimeStylesheet.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/apps/client/src/common/hooks/useRuntimeStylesheet.ts b/apps/client/src/common/hooks/useRuntimeStylesheet.ts index b43c95b4dc..74835f17b6 100644 --- a/apps/client/src/common/hooks/useRuntimeStylesheet.ts +++ b/apps/client/src/common/hooks/useRuntimeStylesheet.ts @@ -27,23 +27,26 @@ export const useRuntimeStylesheet = (): { shouldRender: boolean } => { * @returns { shouldRender: boolean } - after the stylesheet is handled and the clients are ready to render */ useEffect(() => { + let styleSheet = document.getElementById(scriptTagId); + if (!cssData || !data.overrideStyles) { - /** - * No stylesheet was provided, remove any existing stylesheet - */ - document.getElementById(scriptTagId)?.remove(); + // No stylesheet was provided, remove any existing stylesheet + styleSheet?.remove(); setShouldRender(true); return; } setShouldRender(false); - /** - * Add a stylesheet with given content to the document head - */ - const styleSheet = document.createElement('style'); - styleSheet.setAttribute('id', scriptTagId); - styleSheet.innerHTML = cssData; - document.head.append(styleSheet); + + // Ensure the stylesheet is given to the document head + if (!styleSheet) { + styleSheet = document.createElement('style'); + styleSheet.setAttribute('id', scriptTagId); + document.head.append(styleSheet); + } + + // set style sheet content + styleSheet.textContent = cssData; // schedule render for next tick setTimeout(() => setShouldRender(true), 0); From ff3d662e7c1f8c8a20d21d2df9549aebf5071b96 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Mon, 8 Sep 2025 09:14:39 +0200 Subject: [PATCH 17/17] tmp --- .../src/common/hooks/useRuntimeStylesheet.ts | 16 ++++++++-------- apps/client/src/views/ViewLoader.tsx | 9 +++++++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/client/src/common/hooks/useRuntimeStylesheet.ts b/apps/client/src/common/hooks/useRuntimeStylesheet.ts index 74835f17b6..9359431835 100644 --- a/apps/client/src/common/hooks/useRuntimeStylesheet.ts +++ b/apps/client/src/common/hooks/useRuntimeStylesheet.ts @@ -39,14 +39,14 @@ export const useRuntimeStylesheet = (): { shouldRender: boolean } => { setShouldRender(false); // Ensure the stylesheet is given to the document head - if (!styleSheet) { - styleSheet = document.createElement('style'); - styleSheet.setAttribute('id', scriptTagId); - document.head.append(styleSheet); - } - - // set style sheet content - styleSheet.textContent = cssData; + // if (!styleSheet) { + // styleSheet = document.createElement('style'); + // styleSheet.setAttribute('id', scriptTagId); + // document.head.append(styleSheet); + // } + + // // set style sheet content + // styleSheet.textContent = cssData; // schedule render for next tick setTimeout(() => setShouldRender(true), 0); diff --git a/apps/client/src/views/ViewLoader.tsx b/apps/client/src/views/ViewLoader.tsx index b7cb1b3146..9dc7b451f5 100644 --- a/apps/client/src/views/ViewLoader.tsx +++ b/apps/client/src/views/ViewLoader.tsx @@ -1,5 +1,6 @@ import { PropsWithChildren } from 'react'; +import { apiEntryUrl } from '../common/api/constants'; import { useRuntimeStylesheet } from '../common/hooks/useRuntimeStylesheet'; import Loader from './common/loader/Loader'; @@ -15,6 +16,10 @@ export default function ViewLoader({ children }: PropsWithChildren) { return ; } - // eslint-disable-next-line react/jsx-no-useless-fragment -- ensuring JSX return - return <>{children}; + return ( + <> + + {children} + + ); }