From 1b55dbc17043dc474fdd265f55eb79d93f6e0b06 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Mon, 20 Nov 2023 09:53:35 +0100 Subject: [PATCH 1/5] style: clock period (#601) * style: tweaks to studio override * style: improvements to 12 hour format --- .../components/schedule/ScheduleItem.tsx | 16 ++++++++-- .../__tests__/{time.test.js => time.test.ts} | 10 +++++++ .../features/viewers/backstage/Backstage.scss | 1 + .../features/viewers/backstage/Backstage.tsx | 11 +++++-- .../src/features/viewers/clock/Clock.tsx | 8 ++--- .../superscript-time/SuperscriptTime.scss | 4 +++ .../superscript-time/SuperscriptTime.tsx | 23 +++++++++++++++ .../features/viewers/countdown/Countdown.scss | 2 ++ .../features/viewers/countdown/Countdown.tsx | 14 +++++---- .../viewers/countdown/CountdownSelect.tsx | 8 +++-- .../src/features/viewers/public/Public.scss | 1 + .../src/features/viewers/public/Public.tsx | 3 +- .../features/viewers/studio/StudioClock.scss | 29 +++++++++---------- .../src/features/viewers/timer/Timer.scss | 9 +++--- .../src/features/viewers/timer/Timer.tsx | 3 +- apps/server/src/external/styles/override.css | 5 ++++ 16 files changed, 108 insertions(+), 39 deletions(-) rename apps/client/src/common/utils/__tests__/{time.test.js => time.test.ts} (73%) create mode 100644 apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.scss create mode 100644 apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.tsx diff --git a/apps/client/src/common/components/schedule/ScheduleItem.tsx b/apps/client/src/common/components/schedule/ScheduleItem.tsx index af265725c..dcfabc2e2 100644 --- a/apps/client/src/common/components/schedule/ScheduleItem.tsx +++ b/apps/client/src/common/components/schedule/ScheduleItem.tsx @@ -1,7 +1,12 @@ +import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime'; import { formatTime } from '../../utils/time'; import './Schedule.scss'; +const formatOptions = { + format: 'hh:mm a', +}; + interface ScheduleItemProps { selected: 'past' | 'now' | 'future'; timeStart: number; @@ -16,8 +21,8 @@ interface ScheduleItemProps { export default function ScheduleItem(props: ScheduleItemProps) { const { selected, timeStart, timeEnd, title, presenter, backstageEvent, colour, skip } = props; - const start = formatTime(timeStart, { format: 'hh:mm' }); - const end = formatTime(timeEnd, { format: 'hh:mm' }); + const start = formatTime(timeStart, formatOptions); + const end = formatTime(timeEnd, formatOptions); const userColour = colour !== '' ? colour : ''; const selectStyle = `entry--${selected}`; @@ -25,7 +30,12 @@ export default function ScheduleItem(props: ScheduleItemProps) {
  • - {`${start} → ${end} ${backstageEvent ? '*' : ''}`} +
    + + {' → '} + + {backstageEvent ? '*' : ''} +
    {title}
    {presenter &&
    {presenter}
    } diff --git a/apps/client/src/common/utils/__tests__/time.test.js b/apps/client/src/common/utils/__tests__/time.test.ts similarity index 73% rename from apps/client/src/common/utils/__tests__/time.test.js rename to apps/client/src/common/utils/__tests__/time.test.ts index 226d7c2a3..e96d432a7 100644 --- a/apps/client/src/common/utils/__tests__/time.test.js +++ b/apps/client/src/common/utils/__tests__/time.test.ts @@ -26,4 +26,14 @@ describe('formatTime()', () => { const time = formatTime(ms); expect(time).toStrictEqual('...'); }); + + it('shows 12h format without times', () => { + const ms = 13 * 60 * 60 * 1000; + const options = { + showSeconds: false, + format: 'hh:mm a', + }; + const time = formatTime(ms, options, () => '12'); + expect(time).toStrictEqual('01:00 PM'); + }); }); diff --git a/apps/client/src/features/viewers/backstage/Backstage.scss b/apps/client/src/features/viewers/backstage/Backstage.scss index 208ed1aee..45d549e50 100644 --- a/apps/client/src/features/viewers/backstage/Backstage.scss +++ b/apps/client/src/features/viewers/backstage/Backstage.scss @@ -60,6 +60,7 @@ font-weight: 600; color: var(--secondary-color-override, $viewer-secondary-color); letter-spacing: 0.05em; + line-height: 0.95em; } .message { diff --git a/apps/client/src/features/viewers/backstage/Backstage.tsx b/apps/client/src/features/viewers/backstage/Backstage.tsx index 94fb848b1..01e1dab6f 100644 --- a/apps/client/src/features/viewers/backstage/Backstage.tsx +++ b/apps/client/src/features/viewers/backstage/Backstage.tsx @@ -18,6 +18,7 @@ import { TimeManagerType } from '../../../common/models/TimeManager.type'; import { formatTime } from '../../../common/utils/time'; import { useTranslation } from '../../../translation/TranslationProvider'; import { titleVariants } from '../common/animation'; +import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import './Backstage.scss'; @@ -97,7 +98,7 @@ export default function Backstage(props: BackstageProps) { {general.title}
    {getLocalizedString('common.time_now')}
    -
    {clock}
    +
    @@ -128,12 +129,16 @@ export default function Backstage(props: BackstageProps) {
    {getLocalizedString('common.started_at')}
    -
    {startedAt}
    +
    {getLocalizedString('common.expected_finish')}
    -
    {expectedFinish}
    + {isNegative ? ( +
    {expectedFinish}
    + ) : ( + + )}
    diff --git a/apps/client/src/features/viewers/clock/Clock.tsx b/apps/client/src/features/viewers/clock/Clock.tsx index ef40764a2..871a053e8 100644 --- a/apps/client/src/features/viewers/clock/Clock.tsx +++ b/apps/client/src/features/viewers/clock/Clock.tsx @@ -10,6 +10,7 @@ import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet import { TimeManagerType } from '../../../common/models/TimeManager.type'; import { OverridableOptions } from '../../../common/models/View.types'; import { formatTime } from '../../../common/utils/time'; +import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import './Clock.scss'; @@ -135,7 +136,8 @@ export default function Clock(props: ClockProps) { > -
    - {clock} -
    + />
    ); } diff --git a/apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.scss b/apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.scss new file mode 100644 index 000000000..301b9d7da --- /dev/null +++ b/apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.scss @@ -0,0 +1,4 @@ +sup.period { + top: -1em; + font-size: 0.4em; +} diff --git a/apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.tsx b/apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.tsx new file mode 100644 index 000000000..6a1b99054 --- /dev/null +++ b/apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.tsx @@ -0,0 +1,23 @@ +import { CSSProperties } from 'react'; + +import './SuperscriptTime.scss'; + +interface SuperscriptTimeProps { + time: string; + className?: string; + style?: CSSProperties; +} + +export default function SuperscriptTime(props: SuperscriptTimeProps) { + const { time, className, style } = props; + + // we assume anything after space is a period tag + const [timeString, period] = time.split(' '); + + return ( +
    + {timeString} + {period && {period}} +
    + ); +} diff --git a/apps/client/src/features/viewers/countdown/Countdown.scss b/apps/client/src/features/viewers/countdown/Countdown.scss index 7859e460e..c2c33a57b 100644 --- a/apps/client/src/features/viewers/countdown/Countdown.scss +++ b/apps/client/src/features/viewers/countdown/Countdown.scss @@ -68,6 +68,7 @@ font-size: clamp(32px, 3.5vw, 50px); color: var(--secondary-color-override, $viewer-secondary-color); letter-spacing: 0.05em; + line-height: 0.95em; } } @@ -135,6 +136,7 @@ font-size: clamp(32px, 3.5vw, 50px); color: var(--secondary-color-override, $viewer-secondary-color); letter-spacing: 0.05em; + line-height: 0.95em; &--delayed { color: $delay-color; diff --git a/apps/client/src/features/viewers/countdown/Countdown.tsx b/apps/client/src/features/viewers/countdown/Countdown.tsx index 441ded578..b6ac0658b 100644 --- a/apps/client/src/features/viewers/countdown/Countdown.tsx +++ b/apps/client/src/features/viewers/countdown/Countdown.tsx @@ -11,6 +11,7 @@ import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet import { TimeManagerType } from '../../../common/models/TimeManager.type'; import { formatTime } from '../../../common/utils/time'; import { useTranslation } from '../../../translation/TranslationProvider'; +import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import { fetchTimerData, TimerMessage } from './countdown.helpers'; import CountdownSelect from './CountdownSelect'; @@ -118,26 +119,27 @@ export default function Countdown(props: CountdownProps) {
    {getLocalizedString('common.time_now')}
    -
    {clock}
    +
    {runningMessage !== TimerMessage.unhandled && (
    {getLocalizedString(`countdown.${runningMessage}`)}
    )} - - {formattedTimer} - +
    {follow?.title || 'Untitled Event'}
    {getLocalizedString('common.start_time')}
    - {startTime} +
    {getLocalizedString('common.end_time')}
    - {endTime} +
    diff --git a/apps/client/src/features/viewers/countdown/CountdownSelect.tsx b/apps/client/src/features/viewers/countdown/CountdownSelect.tsx index 056a05673..16aa5c9b6 100644 --- a/apps/client/src/features/viewers/countdown/CountdownSelect.tsx +++ b/apps/client/src/features/viewers/countdown/CountdownSelect.tsx @@ -9,6 +9,10 @@ import { sanitiseTitle } from './countdown.helpers'; import './Countdown.scss'; +const formatOptions = { + format: 'hh:mm a', +}; + interface CountdownSelectProps { events: OntimeRundownEntry[]; } @@ -31,8 +35,8 @@ export default function CountdownSelect(props: CountdownSelectProps) { filteredEvents.map((event: OntimeEvent, counter: number) => { const index = counter + 1; const title = sanitiseTitle(event.title); - const start = formatTime(event.timeStart, { format: 'hh:mm' }); - const end = formatTime(event.timeEnd, { format: 'hh:mm' }); + const start = formatTime(event.timeStart, formatOptions); + const end = formatTime(event.timeEnd, formatOptions); return (
  • diff --git a/apps/client/src/features/viewers/public/Public.scss b/apps/client/src/features/viewers/public/Public.scss index 504cb24ce..d38f72110 100644 --- a/apps/client/src/features/viewers/public/Public.scss +++ b/apps/client/src/features/viewers/public/Public.scss @@ -58,6 +58,7 @@ font-size: clamp(32px, 3.5vw, 50px); font-weight: 600; letter-spacing: 0.05em; + line-height: 0.95em; } .message { diff --git a/apps/client/src/features/viewers/public/Public.tsx b/apps/client/src/features/viewers/public/Public.tsx index 78d97a4f4..a9b779e99 100644 --- a/apps/client/src/features/viewers/public/Public.tsx +++ b/apps/client/src/features/viewers/public/Public.tsx @@ -16,6 +16,7 @@ import { TimeManagerType } from '../../../common/models/TimeManager.type'; import { formatTime } from '../../../common/utils/time'; import { useTranslation } from '../../../translation/TranslationProvider'; import { titleVariants } from '../common/animation'; +import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import './Public.scss'; @@ -63,7 +64,7 @@ export default function Public(props: BackstageProps) { {general.title}
    {getLocalizedString('common.time_now')}
    -
    {clock}
    +
    diff --git a/apps/client/src/features/viewers/studio/StudioClock.scss b/apps/client/src/features/viewers/studio/StudioClock.scss index 6421a99f6..cfb997e49 100644 --- a/apps/client/src/features/viewers/studio/StudioClock.scss +++ b/apps/client/src/features/viewers/studio/StudioClock.scss @@ -13,7 +13,7 @@ $half-hours: min(1.5vh, 10px); $size-min: min(2.5vh, 18px); $half-min: min(1.25vh, 9px); $red-active: #c53030; -$red-idle: #300000; +$red-idle: #000000; $cyan-active: #0ff; $cyan-idle: #0aa; @@ -56,11 +56,10 @@ $cyan-idle: #0aa; .hours { border-radius: 50%; position: absolute; - background: $red-idle; + background: var(--studio-idle, $red-idle); &--active { - background: $red-active; - box-shadow: 0 0 10px 2px rgba(255, 0, 0, 0.25); + background: var(--studio-active, $red-active); } } @@ -80,7 +79,7 @@ $cyan-idle: #0aa; } .studio-timer { - color: $red-active; + color: var(--studio-active, $red-active); font-size: calc(#{$clock-size} / 3); margin-top: calc(50% - calc(#{$clock-size} / 7)); line-height: 0.8em; @@ -88,9 +87,8 @@ $cyan-idle: #0aa; &--with-seconds { font-size: calc(#{$clock-size} / 4.6); margin-top: calc(50% - calc(#{$clock-size} / 11)); + } } - } - .next-title:after, @@ -100,14 +98,15 @@ $cyan-idle: #0aa; } .next-title { - color: $cyan-idle; + color: var(--studio-idle-label, $cyan-idle); text-align: center; } .next-countdown { + color: var(--studio-active-label, $cyan-active); + font-size: 10vh; line-height: 1em; - color: $cyan-active; &--overtime { color: darken($red-active, 10%); @@ -131,16 +130,16 @@ $cyan-idle: #0aa; padding-bottom: 2vh; font-size: 15vh; line-height: 0.9em; - color: $red-active; + color: var(--studio-active, $red-active); &--idle { - color: $red-idle; + color: var(--studio-idle, $red-active); } } .schedule { ul { - color: $cyan-idle; + color: var(--studio-idle-label, $cyan-idle); font-size: 3.75vh; line-height: 1em; list-style: none; @@ -154,18 +153,18 @@ $cyan-idle: #0aa; } .now { - color: $cyan-active; + color: var(--studio-active-label, $cyan-active); } .next { - color: $red-active; + color: var(--studio-active, $red-active); } .user-colour { width: 0.35em; height: 0.35em; aspect-ratio: 1; - background-color: $red-idle; + background-color: var(--studio-idle, $red-idle); margin-right: 0.35em; } } diff --git a/apps/client/src/features/viewers/timer/Timer.scss b/apps/client/src/features/viewers/timer/Timer.scss index 5d53cd3cb..e7c3192b5 100644 --- a/apps/client/src/features/viewers/timer/Timer.scss +++ b/apps/client/src/features/viewers/timer/Timer.scss @@ -48,6 +48,7 @@ font-size: clamp(32px, 3.5vw, 50px); color: var(--secondary-color-override, $viewer-secondary-color); letter-spacing: 0.05em; + line-height: 0.95em; } &--hidden { @@ -115,11 +116,11 @@ opacity: 1; transition: $viewer-transition-time; - &--paused { - opacity: $viewer-opacity-disabled; - transition: $viewer-transition-time; + &--paused { + opacity: $viewer-opacity-disabled; + transition: $viewer-transition-time; + } } -} /* =================== OVERLAY ===================*/ diff --git a/apps/client/src/features/viewers/timer/Timer.tsx b/apps/client/src/features/viewers/timer/Timer.tsx index aaccf4c63..914b98328 100644 --- a/apps/client/src/features/viewers/timer/Timer.tsx +++ b/apps/client/src/features/viewers/timer/Timer.tsx @@ -12,6 +12,7 @@ import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet import { TimeManagerType } from '../../../common/models/TimeManager.type'; import { formatTime } from '../../../common/utils/time'; import { useTranslation } from '../../../translation/TranslationProvider'; +import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils'; import './Timer.scss'; @@ -106,7 +107,7 @@ export default function Timer(props: TimerProps) {
    {getLocalizedString('common.time_now')}
    -
    {clock}
    +
    diff --git a/apps/server/src/external/styles/override.css b/apps/server/src/external/styles/override.css index e4ab0deec..63c304c7d 100644 --- a/apps/server/src/external/styles/override.css +++ b/apps/server/src/external/styles/override.css @@ -16,6 +16,11 @@ --operator-running-bg-override: #D20300; --operator-highlight-override: #FFAB33; + + --studio-active: #101010; + --studio-idle: #cfcfcf; + --studio-active-label: #101010; + --studio-idle-label: #595959; } .timer { From afa62a6e380fe0b971baf1e239ae0c15e1aad5b3 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:45:17 +0100 Subject: [PATCH 2/5] test: alias feature (#602) --- .../modals/settings-modal/AliasesForm.tsx | 11 +++++++++- e2e/tests/features/206-alias.spec.ts | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 e2e/tests/features/206-alias.spec.ts diff --git a/apps/client/src/features/modals/settings-modal/AliasesForm.tsx b/apps/client/src/features/modals/settings-modal/AliasesForm.tsx index cf3e1f12e..7937e8b5e 100644 --- a/apps/client/src/features/modals/settings-modal/AliasesForm.tsx +++ b/apps/client/src/features/modals/settings-modal/AliasesForm.tsx @@ -112,6 +112,7 @@ export default function AliasesForm() { icon={} colorScheme='red' isDisabled={disableInputs} + data-testid={`field__delete_${index}`} /> handleLinks(event, alias.alias)} @@ -140,8 +143,14 @@ export default function AliasesForm() { icon={} colorScheme='red' isDisabled={disableInputs} + data-testid={`field__test_${index}`} + /> + -
  • ); })} diff --git a/e2e/tests/features/206-alias.spec.ts b/e2e/tests/features/206-alias.spec.ts new file mode 100644 index 000000000..2888c5ab5 --- /dev/null +++ b/e2e/tests/features/206-alias.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@playwright/test'; + +test('test aliases feature, it should redirect to given alias', async ({ page }) => { + await page.goto('http://localhost:4001/editor'); + + // open settings + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'URL Aliases' }).click(); + + // create alias + await page.getByRole('button', { name: 'Add new' }).click(); + await page.getByTestId('field__alias_1').fill('testing'); + await page.getByTestId('field__url_1').fill('countdown'); + await page.getByTestId('field__enable_1').click(); + await page.getByRole('button', { name: 'Save', exact: true }).click(); + await page.getByRole('button', { name: 'Close' }).click(); + + // make sure alias works + await page.goto('http://localhost:4001/testing'); + await page.getByText('Select an event to follow').click(); +}); From a79ae7e0c4459496da07ad2aa1bbdbb17a617372 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Mon, 20 Nov 2023 21:11:52 +0100 Subject: [PATCH 3/5] fix: cuesheet header (#600) * `useLocalStorage` hook uses `useSyncExternalStore` --------- Co-authored-by: asharonbaltazar --- .../src/common/hooks/useLocalStorage.ts | 75 +++++++++---------- .../client/src/features/cuesheet/Cuesheet.tsx | 13 ++-- .../CuesheetHeader.tsx | 4 +- .../CuesheetTableSettings.tsx | 14 ++-- 4 files changed, 50 insertions(+), 56 deletions(-) diff --git a/apps/client/src/common/hooks/useLocalStorage.ts b/apps/client/src/common/hooks/useLocalStorage.ts index a6468e7af..3519821b9 100644 --- a/apps/client/src/common/hooks/useLocalStorage.ts +++ b/apps/client/src/common/hooks/useLocalStorage.ts @@ -1,53 +1,46 @@ -import { useEffect, useState } from 'react'; +import { useSyncExternalStore } from 'react'; -/** - * @description utility hook to handle state in local storage - * @param key - * @param initialValue - */ -export const useLocalStorage = (key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] => { - const [storedValue, setStoredValue] = useState(() => { - try { - const item = window.localStorage.getItem(`ontime-${key}`); - return item ? JSON.parse(item) : initialValue; - } catch (error) { - return initialValue; - } - }); +const STORAGE_EVENT = 'ontime-storage'; - useEffect(() => { - const handleStorageChange = (event: StorageEvent) => { - if (event.storageArea === window.localStorage && event.key === key) { - try { - const newValue = event.newValue ? JSON.parse(event.newValue) : initialValue; - setStoredValue(newValue); - } catch (_) { - /* empty */ - } - } - }; +function getSnapshot(key: string): string | null { + try { + return window.localStorage.getItem(`ontime-${key}`); + } catch { + return null; + } +} - window.addEventListener('storage', handleStorageChange); +function getParsedJson(localStorageValue: string | null, initialValue: T): T { + try { + return localStorageValue ? JSON.parse(localStorageValue) : initialValue; + } catch { + return initialValue; + } +} - return () => { - window.removeEventListener('storage', handleStorageChange); - }; - }, [initialValue, key]); +export const useLocalStorage = (key: string, initialValue: T) => { + const localStorageValue = useSyncExternalStore(subscribe, () => getSnapshot(key)); + const parsedLocalStorageValue = getParsedJson(localStorageValue, initialValue); /** * @description Set value to local storage * @param value */ - const setValue = (value: T | ((val: T) => T)) => { - try { - // Allow value to be a function so we have same API as useState - const valueToStore = value instanceof Function ? value(storedValue) : value; + const setLocalStorageValue = (value: T | ((val: T) => T)) => { + // Allow value to be a function so we have same API as useState + const valueToStore = value instanceof Function ? value(parsedLocalStorageValue) : value; - setStoredValue(valueToStore); - window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore)); - } catch (error) { - console.error(error); - } + localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore)); + window.dispatchEvent(new StorageEvent(STORAGE_EVENT)); }; - return [storedValue, setValue]; + + return [parsedLocalStorageValue, setLocalStorageValue] as const; }; + +function subscribe(callback: () => void) { + window.addEventListener(STORAGE_EVENT, callback); + + return () => { + window.removeEventListener(STORAGE_EVENT, callback); + }; +} diff --git a/apps/client/src/features/cuesheet/Cuesheet.tsx b/apps/client/src/features/cuesheet/Cuesheet.tsx index a674ec79d..e9e00ed6a 100644 --- a/apps/client/src/features/cuesheet/Cuesheet.tsx +++ b/apps/client/src/features/cuesheet/Cuesheet.tsx @@ -24,10 +24,7 @@ interface CuesheetProps { } export default function Cuesheet({ data, columns, handleUpdate, selectedId }: CuesheetProps) { - const followSelected = useCuesheetSettings((state) => state.followSelected); - const showSettings = useCuesheetSettings((state) => state.showSettings); - const showDelayBlock = useCuesheetSettings((state) => state.showDelayBlock); - const showPrevious = useCuesheetSettings((state) => state.showPrevious); + const { followSelected, showSettings, showDelayBlock, showPrevious } = useCuesheetSettings(); const [columnVisibility, setColumnVisibility] = useLocalStorage('table-hidden', {}); const [columnOrder, saveColumnOrder] = useLocalStorage('table-order', initialColumnOrder); @@ -66,7 +63,9 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu setColumnSizing({}); }; - const headerGroups = table.getHeaderGroups; + const headerGroups = table.getHeaderGroups(); + const rowModel = table.getRowModel(); + const allLeafColumns = table.getAllLeafColumns(); let eventIndex = 0; let isPast = Boolean(selectedId); @@ -75,7 +74,7 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu <> {showSettings && ( - {table.getRowModel().rows.map((row) => { + {rowModel.rows.map((row) => { const key = row.original.id; const isSelected = selectedId === key; if (isSelected) { diff --git a/apps/client/src/features/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx b/apps/client/src/features/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx index a340fc098..91d8e8248 100644 --- a/apps/client/src/features/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx +++ b/apps/client/src/features/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx @@ -23,7 +23,7 @@ import { SortableCell } from './SortableCell'; import style from '../Cuesheet.module.scss'; interface CuesheetHeaderProps { - headerGroups: () => HeaderGroup[]; + headerGroups: HeaderGroup[]; } function CuesheetHeader(props: CuesheetHeaderProps) { @@ -75,7 +75,7 @@ function CuesheetHeader(props: CuesheetHeaderProps) { return ( - {headerGroups().map((headerGroup) => { + {headerGroups.map((headerGroup) => { const key = headerGroup.id; return ( diff --git a/apps/client/src/features/cuesheet/cuesheet-table-settings/CuesheetTableSettings.tsx b/apps/client/src/features/cuesheet/cuesheet-table-settings/CuesheetTableSettings.tsx index e292a681c..722d41a97 100644 --- a/apps/client/src/features/cuesheet/cuesheet-table-settings/CuesheetTableSettings.tsx +++ b/apps/client/src/features/cuesheet/cuesheet-table-settings/CuesheetTableSettings.tsx @@ -22,12 +22,14 @@ interface CuesheetTableSettingsProps { function CuesheetTableSettings(props: CuesheetTableSettingsProps) { const { columns, handleResetResizing, handleResetReordering, handleClearToggles } = props; - const showPrevious = useCuesheetSettings((state) => state.showPrevious); - const togglePreviousVisibility = useCuesheetSettings((state) => state.togglePreviousVisibility); - const showDelayBlock = useCuesheetSettings((state) => state.showDelayBlock); - const toggleDelayVisibility = useCuesheetSettings((state) => state.toggleDelayVisibility); - const showDelayedTimes = useCuesheetSettings((state) => state.showDelayedTimes); - const toggleDelayedTimes = useCuesheetSettings((state) => state.toggleDelayedTimes); + const { + showPrevious, + toggleDelayVisibility, + showDelayBlock, + showDelayedTimes, + toggleDelayedTimes, + togglePreviousVisibility, + } = useCuesheetSettings(); return (
    From c4886a617ce0ef8fa01658b69b0e56254527121b Mon Sep 17 00:00:00 2001 From: asharonbaltazar <58940073+asharonbaltazar@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:41:41 -0500 Subject: [PATCH 4/5] remove `storedViewParams` from the dep array (#606) --- .../components/view-params-editor/ViewParamsEditor.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx index cb0ba3d55..426b97f9a 100644 --- a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx +++ b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx @@ -59,10 +59,10 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) { setSearchParams(defaultSearchParams); } - // linter is asking for `setSearchParams` in the useEffect deps - // rule is disabled since adding `setSearchParams` results in unnecessary re-renders + // linter is asking for `setSearchParams` & `storedViewParams` in the useEffect deps + // rule is disabled since adding `setSearchParams` & `storedViewParams` results in unnecessary re-renders // eslint-disable-next-line react-hooks/exhaustive-deps - }, [storedViewParams, pathname]); + }, [pathname]); const onEditDrawerClose = () => { onClose(); From 797a0edf5718aee2616324b7d4e3d01a6ef3d023 Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Wed, 22 Nov 2023 08:06:48 +0100 Subject: [PATCH 5/5] External field (#580) * feat: add external endpoint * add external to messagecontrol --------- Co-authored-by: Carlos Valente --- apps/client/src/common/hooks/useSocket.ts | 3 + apps/client/src/common/stores/runtime.ts | 6 +- .../src/features/control/message/InputRow.tsx | 35 +++++++---- .../message/MessageControl.module.scss | 4 -- .../control/message/MessageControl.tsx | 32 +++++++--- .../src/features/viewers/ViewWrapper.tsx | 6 +- .../src/features/viewers/timer/Timer.scss | 29 ++++++++- .../src/features/viewers/timer/Timer.tsx | 23 ++++++-- apps/client/src/theme/_viewerDefs.scss | 2 +- apps/client/src/theme/ontimeTextInputs.ts | 15 +++++ apps/server/src/app.ts | 4 ++ .../src/controllers/integrationController.ts | 14 +++++ apps/server/src/external/styles/override.css | 2 + .../message-service/MessageService.ts | 59 +++++++++++++++---- apps/server/src/stores/EventStore.ts | 3 + apps/server/src/utils/throttle.ts | 32 ++++++++++ .../features/201-message-control.spec.ts | 8 +-- .../definitions/runtime/RuntimeStore.type.ts | 1 + 18 files changed, 229 insertions(+), 49 deletions(-) create mode 100644 apps/server/src/utils/throttle.ts diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index f2d5fbb5f..4ce6edc2b 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -27,6 +27,7 @@ export const useMessageControl = () => { timerMessage: state.timerMessage, publicMessage: state.publicMessage, lowerMessage: state.lowerMessage, + externalMessage: state.externalMessage, onAir: state.onAir, }); @@ -40,6 +41,8 @@ export const setMessage = { publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload), lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload), lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload), + externalText: (payload: string) => socketSendJson('set-external-message-text', payload), + externalVisible: (payload: boolean) => socketSendJson('set-external-message-visible', payload), onAir: (payload: boolean) => socketSendJson('set-onAir', payload), timerBlink: (payload: boolean) => socketSendJson('set-timer-blink', payload), timerBlackout: (payload: boolean) => socketSendJson('set-timer-blackout', payload), diff --git a/apps/client/src/common/stores/runtime.ts b/apps/client/src/common/stores/runtime.ts index 3f503308d..98a04c134 100644 --- a/apps/client/src/common/stores/runtime.ts +++ b/apps/client/src/common/stores/runtime.ts @@ -3,7 +3,7 @@ import { Playback, RuntimeStore } from 'ontime-types'; import { useStore } from 'zustand'; import { createStore } from 'zustand/vanilla'; -export const runtimeStorePlaceholder = { +export const runtimeStorePlaceholder: RuntimeStore = { timer: { clock: 0, current: null, @@ -33,6 +33,10 @@ export const runtimeStorePlaceholder = { text: '', visible: false, }, + externalMessage: { + text: '', + visible: false, + }, onAir: false, loaded: { numEvents: 0, diff --git a/apps/client/src/features/control/message/InputRow.tsx b/apps/client/src/features/control/message/InputRow.tsx index 9683dec02..561c19740 100644 --- a/apps/client/src/features/control/message/InputRow.tsx +++ b/apps/client/src/features/control/message/InputRow.tsx @@ -1,4 +1,4 @@ -import { Input } from '@chakra-ui/react'; +import { IconButton, Input } from '@chakra-ui/react'; import { IoEye } from '@react-icons/all-files/io5/IoEye'; import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline'; @@ -13,13 +13,14 @@ interface InputRowProps { placeholder: string; text: string; visible?: boolean; + readonly?: boolean; actionHandler: (action: string, payload: object) => void; changeHandler: (newValue: string) => void; className?: string; } export default function InputRow(props: InputRowProps) { - const { label, placeholder, text, visible, actionHandler, changeHandler, className } = props; + const { label, placeholder, text, visible, actionHandler, changeHandler, className, readonly } = props; const handleInputChange = (newValue: string) => { changeHandler(newValue); @@ -33,19 +34,31 @@ export default function InputRow(props: InputRowProps) { handleInputChange(event.target.value)} placeholder={placeholder} /> - actionHandler('update', { field: 'isPublic', value: !visible })} - tooltip={visible ? 'Make invisible' : 'Make visible'} - aria-label={`Toggle ${label}`} - openDelay={tooltipDelayMid} - icon={visible ? : } - variant={visible ? 'ontime-filled' : 'ontime-subtle'} - size='sm' - /> + {readonly ? ( + : } + aria-label={`Toggle ${label}`} + variant={visible ? 'ontime-filled' : 'ontime-subtle'} + /> + ) : ( + actionHandler('update', { field: 'isPublic', value: !visible })} + tooltip={visible ? 'Make invisible' : 'Make visible'} + aria-label={`Toggle ${label}`} + openDelay={tooltipDelayMid} + icon={visible ? : } + variant={visible ? 'ontime-filled' : 'ontime-subtle'} + size='sm' + /> + )}
    ); diff --git a/apps/client/src/features/control/message/MessageControl.module.scss b/apps/client/src/features/control/message/MessageControl.module.scss index ab966b116..5f3d0e46f 100644 --- a/apps/client/src/features/control/message/MessageControl.module.scss +++ b/apps/client/src/features/control/message/MessageControl.module.scss @@ -27,7 +27,3 @@ color: $action-text-color; } } - -.padTop { - margin-top: $section-spacing; -} diff --git a/apps/client/src/features/control/message/MessageControl.tsx b/apps/client/src/features/control/message/MessageControl.tsx index 73138367b..8feb66cc9 100644 --- a/apps/client/src/features/control/message/MessageControl.tsx +++ b/apps/client/src/features/control/message/MessageControl.tsx @@ -34,34 +34,48 @@ export default function MessageControl() { actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)} /> setMessage.presenterText(newValue)} actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)} />
    - -
    -
    + undefined} + actionHandler={() => undefined} + /> +
    -
    +
    {showEndMessage ? (
    {viewSettings.endMessage}
    ) : ( @@ -124,6 +131,12 @@ export default function Timer(props: TimerProps) { {display}
    )} +
    + {external.text} +
    { const initialPayload = getInitialPayload(); eventStore.init(initialPayload); + // eventStore set is a dependency of the services that publish to it + messageService.init(eventStore.set.bind(eventStore)); + expressServer.listen(serverPort, '0.0.0.0'); return { message: returnMessage, serverPort }; diff --git a/apps/server/src/controllers/integrationController.ts b/apps/server/src/controllers/integrationController.ts index a8f5bd95d..a37579a37 100644 --- a/apps/server/src/controllers/integrationController.ts +++ b/apps/server/src/controllers/integrationController.ts @@ -109,6 +109,20 @@ export function dispatchFromAdapter( break; } + case 'set-external-message-text': { + if (typeof payload !== 'string') { + throw new Error(`Unable to parse payload: ${payload}`); + } + messageService.setExternalText(payload); + return; + } + case 'set-external-message-visible': { + if (typeof payload === 'undefined') { + throw new Error(`Unable to parse payload: ${payload}`); + } + messageService.setExternalVisibility(Boolean(payload)); + break; + } case 'start': { PlaybackService.start(); break; diff --git a/apps/server/src/external/styles/override.css b/apps/server/src/external/styles/override.css index 63c304c7d..cc5a7cf21 100644 --- a/apps/server/src/external/styles/override.css +++ b/apps/server/src/external/styles/override.css @@ -12,6 +12,8 @@ --timer-progress-bg-override: #fff; --timer-progress-override: #202020; + --external-color-override: #161616; + --cuesheet-running-bg-override: #D20300; --operator-running-bg-override: #D20300; diff --git a/apps/server/src/services/message-service/MessageService.ts b/apps/server/src/services/message-service/MessageService.ts index c1878cee8..5ab3bf048 100644 --- a/apps/server/src/services/message-service/MessageService.ts +++ b/apps/server/src/services/message-service/MessageService.ts @@ -1,7 +1,9 @@ import { Message } from 'ontime-types'; -import { eventStore } from '../../stores/EventStore.js'; import { TimerMessage } from 'ontime-types/src/definitions/runtime/MessageControl.type.js'; +import { throttle } from '../../utils/throttle.js'; + +import type { PublishFn } from '../../stores/EventStore.js'; let instance; @@ -9,8 +11,12 @@ class MessageService { timerMessage: TimerMessage; publicMessage: Message; lowerMessage: Message; + externalMessage: Message; onAir: boolean; + private throttledSet: PublishFn; + private publish: PublishFn | null; + constructor() { if (instance) { throw new Error('There can be only one'); @@ -36,7 +42,40 @@ class MessageService { visible: false, }; + this.externalMessage = { + text: '', + visible: false, + }; + this.onAir = false; + this.throttledSet = () => { + throw new Error('Published called before initialisation'); + }; + } + + init(publish: PublishFn) { + this.publish = publish; + this.throttledSet = throttle((key, value) => this.publish(key, value), 100); + } + + /** + * @description sets message on stage timer screen + */ + setExternalText(payload: string) { + if (this.externalMessage.text !== payload) { + this.externalMessage.text = payload; + this.throttledSet('externalMessage', this.externalMessage); + } + return this.getAll(); + } + + /** + * @description sets message visibility on stage timer screen + */ + setExternalVisibility(status: boolean) { + this.externalMessage.visible = status; + this.throttledSet('externalMessage', this.externalMessage); + return this.getAll(); } /** @@ -44,7 +83,7 @@ class MessageService { */ setTimerText(payload: string) { this.timerMessage.text = payload; - eventStore.set('timerMessage', this.timerMessage); + this.throttledSet('timerMessage', this.timerMessage); return this.getAll(); } @@ -53,7 +92,7 @@ class MessageService { */ setTimerVisibility(status: boolean) { this.timerMessage.visible = status; - eventStore.set('timerMessage', this.timerMessage); + this.throttledSet('timerMessage', this.timerMessage); return this.getAll(); } @@ -62,7 +101,7 @@ class MessageService { */ setPublicText(payload: string) { this.publicMessage.text = payload; - eventStore.set('publicMessage', this.publicMessage); + this.throttledSet('publicMessage', this.publicMessage); return this.getAll(); } @@ -71,7 +110,7 @@ class MessageService { */ setPublicVisibility(status: boolean) { this.publicMessage.visible = status; - eventStore.set('publicMessage', this.publicMessage); + this.throttledSet('publicMessage', this.publicMessage); return this.getAll(); } @@ -80,7 +119,7 @@ class MessageService { */ setLowerText(payload: string) { this.lowerMessage.text = payload; - eventStore.set('lowerMessage', this.lowerMessage); + this.throttledSet('lowerMessage', this.lowerMessage); return this.getAll(); } @@ -89,7 +128,7 @@ class MessageService { */ setLowerVisibility(status: boolean) { this.lowerMessage.visible = status; - eventStore.set('lowerMessage', this.lowerMessage); + this.throttledSet('lowerMessage', this.lowerMessage); return this.getAll(); } @@ -102,7 +141,7 @@ class MessageService { } else { this.onAir = status; } - eventStore.set('onAir', this.onAir); + this.throttledSet('onAir', this.onAir); return this.getAll(); } @@ -116,7 +155,7 @@ class MessageService { } else { this.timerMessage.timerBlink = status; } - eventStore.set('timerMessage', this.timerMessage); + this.throttledSet('timerMessage', this.timerMessage); return this.getAll(); } @@ -130,7 +169,7 @@ class MessageService { } else { this.timerMessage.timerBlackout = status; } - eventStore.set('timerMessage', this.timerMessage); + this.throttledSet('timerMessage', this.timerMessage); return this.getAll(); } diff --git a/apps/server/src/stores/EventStore.ts b/apps/server/src/stores/EventStore.ts index 19e59ded9..a6410b0bf 100644 --- a/apps/server/src/stores/EventStore.ts +++ b/apps/server/src/stores/EventStore.ts @@ -4,6 +4,8 @@ import { eventTimer } from '../services/TimerService.js'; import { messageService } from '../services/message-service/MessageService.js'; import { eventLoader } from '../classes/event-loader/EventLoader.js'; +export type PublishFn = (key: T, value: RuntimeStore[T]) => void; + let store: Partial = {}; /** @@ -68,6 +70,7 @@ export const getInitialPayload = () => ({ timerMessage: messageService.timerMessage, publicMessage: messageService.publicMessage, lowerMessage: messageService.lowerMessage, + externalMessage: messageService.externalMessage, onAir: messageService.onAir, loaded: eventLoader.loaded, eventNow: eventLoader.eventNow, diff --git a/apps/server/src/utils/throttle.ts b/apps/server/src/utils/throttle.ts new file mode 100644 index 000000000..275d9772b --- /dev/null +++ b/apps/server/src/utils/throttle.ts @@ -0,0 +1,32 @@ +/** + * Creates a throttled version of the passed function + * This function uses a leading algorithm + * which means that the function will be executed immediately on first call + * @param {Function} cb - function to throttle + * @param {number} delay - time (in ms) to throttle + * @returns {Function} + */ +export function throttle(cb: (...args: T) => U, delay: number) { + let shouldWait = false; + let waitingArgs; + const timeoutFunc = () => { + if (waitingArgs == null) { + shouldWait = false; + } else { + cb(...waitingArgs); + waitingArgs = null; + setTimeout(timeoutFunc, delay); + } + }; + + return (...args: T) => { + if (shouldWait) { + waitingArgs = args; + return; + } + + cb(...args); + shouldWait = true; + setTimeout(timeoutFunc, delay); + }; +} diff --git a/e2e/tests/features/201-message-control.spec.ts b/e2e/tests/features/201-message-control.spec.ts index b36d3c85a..dbe55e417 100644 --- a/e2e/tests/features/201-message-control.spec.ts +++ b/e2e/tests/features/201-message-control.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -test('test', async ({ context }) => { +test('message control sends messages to screens', async ({ context }) => { const editorPage = await context.newPage(); const featurePage = await context.newPage(); @@ -25,9 +25,9 @@ test('test', async ({ context }) => { await featurePage.getByText('testing lower').click({ timeout: 5000 }); // stage timer message - await editorPage.getByPlaceholder('Shown in stage timer').click(); - await editorPage.getByPlaceholder('Shown in stage timer').fill('testing stage'); - await editorPage.getByRole('button', { name: /toggle timer message/i }).click({ timeout: 5000 }); + await editorPage.getByPlaceholder('Timer').click(); + await editorPage.getByPlaceholder('Timer').fill('testing stage'); + await editorPage.getByRole('button', { name: /toggle timer/i }).click({ timeout: 5000 }); await featurePage.goto('http://localhost:4001/timer'); await featurePage.waitForLoadState('load', { timeout: 5000 }); diff --git a/packages/types/src/definitions/runtime/RuntimeStore.type.ts b/packages/types/src/definitions/runtime/RuntimeStore.type.ts index d9f68cbba..37d1c2d03 100644 --- a/packages/types/src/definitions/runtime/RuntimeStore.type.ts +++ b/packages/types/src/definitions/runtime/RuntimeStore.type.ts @@ -13,6 +13,7 @@ export type RuntimeStore = { timerMessage: TimerMessage; publicMessage: Message; lowerMessage: Message; + externalMessage: Message; onAir: boolean; // event loader