From 3998b8927da369d133255fff145522c0e80bcdc1 Mon Sep 17 00:00:00 2001 From: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> Date: Sat, 30 Dec 2023 20:56:57 +0100 Subject: [PATCH] Warning and danger per event (#677) * feat: warning and danger per event --------- Co-authored-by: Fabian Posenau --- .../input/time-input/TimeInput.module.scss | 31 +----- .../components/input/time-input/TimeInput.tsx | 86 +++++----------- .../TimeInputWithButton.module.scss | 37 +++++++ .../input/time-input/TimeInputWithButton.tsx | 97 +++++++++++++++++++ .../MultiPartProgressBar.tsx | 8 +- .../src/common/models/TimeManager.type.ts | 3 + .../src/common/models/ViewSettings.type.ts | 2 - apps/client/src/common/stores/runtime.ts | 2 + .../utils/__tests__/eventsManager.test.ts | 4 + apps/client/src/common/utils/eventsManager.ts | 2 + apps/client/src/common/utils/timesManager.ts | 2 +- .../cuesheet-progress/CuesheetProgress.tsx | 6 +- .../src/features/event-editor/EventEditor.tsx | 2 + .../composite/EventEditorTimes.tsx | 56 +++++++++-- .../settings-modal/InputMillisWithString.tsx | 35 ------- .../settings-modal/ViewSettingsForm.tsx | 36 +------ .../operator/status-bar/StatusBarProgress.tsx | 4 +- .../composite/EventBlockTimers.tsx | 10 +- .../viewers/minimal-timer/MinimalTimer.tsx | 4 +- .../src/features/viewers/timer/Timer.tsx | 8 +- apps/server/src/models/dataModel.ts | 2 - apps/server/src/models/eventsDefinition.ts | 2 + apps/server/src/services/TimerService.ts | 6 ++ .../__tests__/delayedRundown.utils.test.ts | 24 +++++ .../server/src/utils/__tests__/parser.test.ts | 4 - apps/server/src/utils/parser.ts | 10 ++ apps/server/src/utils/parserFunctions.ts | 2 - .../src/definitions/core/OntimeEvent.type.ts | 2 + .../types/src/definitions/core/Views.type.ts | 2 - .../definitions/runtime/TimerState.type.ts | 2 + .../src/feature/excel-import/excelImport.ts | 2 + 31 files changed, 290 insertions(+), 203 deletions(-) create mode 100644 apps/client/src/common/components/input/time-input/TimeInputWithButton.module.scss create mode 100644 apps/client/src/common/components/input/time-input/TimeInputWithButton.tsx delete mode 100644 apps/client/src/features/modals/settings-modal/InputMillisWithString.tsx diff --git a/apps/client/src/common/components/input/time-input/TimeInput.module.scss b/apps/client/src/common/components/input/time-input/TimeInput.module.scss index ff0ef3acf..a0db0b925 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.module.scss +++ b/apps/client/src/common/components/input/time-input/TimeInput.module.scss @@ -4,34 +4,7 @@ $input-font-size: 15px; $input-delayed-border-color: #E69056; .timeInput { - width: fit-content !important; - - .inputLeft { - max-width: fit-content; - } - - .inputLeft, - .inputButton { - aspect-ratio: 1; - } - - .inputField { font-size: $input-font-size; letter-spacing: 1px; - width: 7.5em; - padding: 0 0 0 2.6em; - } - - .warn { - &::after { - content: "*"; - color: $warning-orange; - } - } - - &.delayed { - .inputField { - border: 1px solid $input-delayed-border-color; - } - } -} + width: 6.5em; +} \ No newline at end of file diff --git a/apps/client/src/common/components/input/time-input/TimeInput.tsx b/apps/client/src/common/components/input/time-input/TimeInput.tsx index d584b8f02..739fa6088 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.tsx +++ b/apps/client/src/common/components/input/time-input/TimeInput.tsx @@ -1,15 +1,12 @@ -import { FocusEvent, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react'; +import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react'; +import { Input } from '@chakra-ui/react'; import { millisToString } from 'ontime-utils'; -import { tooltipDelayFast } from '../../../../ontimeConfig'; import { useEmitLog } from '../../../stores/logger'; import { forgivingStringToMillis } from '../../../utils/dateConfig'; -import { cx } from '../../../utils/styleUtils'; import { TimeEntryField } from '../../../utils/timesManager'; import style from './TimeInput.module.scss'; - interface TimeInputProps { id?: TimeEntryField; name: TimeEntryField; @@ -20,20 +17,7 @@ interface TimeInputProps { validationHandler: (entry: TimeEntryField, val: number) => boolean; previousEnd?: number; warning?: string; -} - -function ButtonInitial(name: TimeEntryField) { - if (name === 'timeStart') return 'S'; - if (name === 'timeEnd') return 'E'; - if (name === 'durationOverride') return 'D'; - return ''; -} - -function ButtonTooltip(name: TimeEntryField, warning?: string) { - if (name === 'timeStart') return `Start${warning ? `: ${warning}` : ''}`; - if (name === 'timeEnd') return `End${warning ? `: ${warning}` : ''}`; - if (name === 'durationOverride') return `Duration${warning ? `: ${warning}` : ''}`; - return ''; + className?: string; } export default function TimeInput(props: TimeInputProps) { @@ -46,7 +30,7 @@ export default function TimeInput(props: TimeInputProps) { placeholder, validationHandler, previousEnd = 0, - warning, + className = '', } = props; const { emitError } = useEmitLog(); const inputRef = useRef(null); @@ -169,51 +153,25 @@ export default function TimeInput(props: TimeInputProps) { resetValue(); }, [resetValue, time]); - const isDelayed = delay !== 0; - const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]); - const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]); - - const TooltipLabel = useMemo(() => { - return ButtonTooltip(name, warning); - }, [name, warning]); - - const ButtonText = useMemo(() => { - return ButtonInitial(name); - }, [name]); + const timeInputClass = className ? className : style.timeInput; return ( - - - - - - - setValue(event.target.value)} - onBlur={onBlurHandler} - onKeyDown={onKeyDownHandler} - value={value} - maxLength={8} - autoComplete='off' - /> - + setValue(event.target.value)} + onBlur={onBlurHandler} + onKeyDown={onKeyDownHandler} + value={value} + maxLength={8} + autoComplete='off' + /> ); } diff --git a/apps/client/src/common/components/input/time-input/TimeInputWithButton.module.scss b/apps/client/src/common/components/input/time-input/TimeInputWithButton.module.scss new file mode 100644 index 000000000..ff0ef3acf --- /dev/null +++ b/apps/client/src/common/components/input/time-input/TimeInputWithButton.module.scss @@ -0,0 +1,37 @@ +@use "../../../../theme/v2Styles" as *; + +$input-font-size: 15px; +$input-delayed-border-color: #E69056; + +.timeInput { + width: fit-content !important; + + .inputLeft { + max-width: fit-content; + } + + .inputLeft, + .inputButton { + aspect-ratio: 1; + } + + .inputField { + font-size: $input-font-size; + letter-spacing: 1px; + width: 7.5em; + padding: 0 0 0 2.6em; + } + + .warn { + &::after { + content: "*"; + color: $warning-orange; + } + } + + &.delayed { + .inputField { + border: 1px solid $input-delayed-border-color; + } + } +} diff --git a/apps/client/src/common/components/input/time-input/TimeInputWithButton.tsx b/apps/client/src/common/components/input/time-input/TimeInputWithButton.tsx new file mode 100644 index 000000000..6e32948ee --- /dev/null +++ b/apps/client/src/common/components/input/time-input/TimeInputWithButton.tsx @@ -0,0 +1,97 @@ +import { useMemo } from 'react'; +import { Button, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react'; + +import { tooltipDelayFast } from '../../../../ontimeConfig'; +import { cx } from '../../../utils/styleUtils'; +import { TimeEntryField } from '../../../utils/timesManager'; + +import TimeInput from './TimeInput'; + +import style from './TimeInputWithButton.module.scss'; + +interface TimeInputProps { + id?: TimeEntryField; + name: TimeEntryField; + submitHandler: (field: TimeEntryField, value: number) => void; + time?: number; + delay?: number; + placeholder: string; + validationHandler: (entry: TimeEntryField, val: number) => boolean; + previousEnd?: number; + warning?: string; +} + +function ButtonInitial(name: TimeEntryField) { + if (name === 'timeStart') return 'S'; + if (name === 'timeEnd') return 'E'; + if (name === 'durationOverride') return 'D'; + if (name === 'timeWarning') return 'Wa'; + if (name === 'timeDanger') return 'Da'; + return ''; +} + +function ButtonTooltip(name: TimeEntryField, warning?: string) { + if (name === 'timeStart') return `Start${warning ? `: ${warning}` : ''}`; + if (name === 'timeEnd') return `End${warning ? `: ${warning}` : ''}`; + if (name === 'durationOverride') return `Duration${warning ? `: ${warning}` : ''}`; + if (name === 'timeWarning') return `Warning${warning ? `: ${warning}` : ''}`; + if (name === 'timeDanger') return `Danger${warning ? `: ${warning}` : ''}`; + return ''; +} + +export default function TimeInputWithButton(props: TimeInputProps) { + const { + id, + name, + submitHandler, + time = 0, + delay = 0, + placeholder, + validationHandler, + previousEnd = 0, + warning, + } = props; + + const isDelayed = delay !== 0; + const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]); + const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]); + + const TooltipLabel = useMemo(() => { + return ButtonTooltip(name, warning); + }, [name, warning]); + + const ButtonText = useMemo(() => { + return ButtonInitial(name); + }, [name]); + + return ( + + + + + + + + + ); +} diff --git a/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx b/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx index 151e3efad..90268367b 100644 --- a/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx +++ b/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx @@ -6,9 +6,9 @@ interface MultiPartProgressBar { now: number | null; complete: number; normalColor: string; - warning: number; + warning?: number | null; warningColor: string; - danger: number; + danger?: number | null; dangerColor: string; hidden?: boolean; className?: string; @@ -19,8 +19,8 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) { const percentComplete = 100 - clamp(100 - (Math.max(now ?? 0, 0) * 100) / complete, 0, 100); - const dangerWidth = clamp((danger / complete) * 100, 0, 100); - const warningWidth = clamp((warning / complete) * 100, 0, 100); + const dangerWidth = danger ? clamp((danger / complete) * 100, 0, 100) : 0; + const warningWidth = warning ? clamp((warning / complete) * 100, 0, 100) : 0; return (
diff --git a/apps/client/src/common/models/TimeManager.type.ts b/apps/client/src/common/models/TimeManager.type.ts index 7e1860395..61b53f101 100644 --- a/apps/client/src/common/models/TimeManager.type.ts +++ b/apps/client/src/common/models/TimeManager.type.ts @@ -15,4 +15,7 @@ export type TimeManagerType = { finished: boolean; playback: Playback; + + timeWarning: number | null; + timeDanger: number | null; }; diff --git a/apps/client/src/common/models/ViewSettings.type.ts b/apps/client/src/common/models/ViewSettings.type.ts index d2d72b519..c04318548 100644 --- a/apps/client/src/common/models/ViewSettings.type.ts +++ b/apps/client/src/common/models/ViewSettings.type.ts @@ -4,8 +4,6 @@ export const viewsSettingsPlaceholder: ViewSettings = { overrideStyles: false, normalColor: '#ffffffcc', warningColor: '#FFAB33', - warningThreshold: 120000, dangerColor: '#ED3333', - dangerThreshold: 60000, endMessage: '', }; diff --git a/apps/client/src/common/stores/runtime.ts b/apps/client/src/common/stores/runtime.ts index 98a04c134..e4ff70f03 100644 --- a/apps/client/src/common/stores/runtime.ts +++ b/apps/client/src/common/stores/runtime.ts @@ -17,6 +17,8 @@ export const runtimeStorePlaceholder: RuntimeStore = { duration: null, timerType: null, endAction: null, + timeWarning: null, + timeDanger: null, }, playback: Playback.Stop, timerMessage: { diff --git a/apps/client/src/common/utils/__tests__/eventsManager.test.ts b/apps/client/src/common/utils/__tests__/eventsManager.test.ts index 140b9ff3d..36bd2bbd8 100644 --- a/apps/client/src/common/utils/__tests__/eventsManager.test.ts +++ b/apps/client/src/common/utils/__tests__/eventsManager.test.ts @@ -31,6 +31,8 @@ describe('cloneEvent()', () => { user7: 'user7', user8: 'user8', user9: 'user9', + timeWarning: 120000, + timeDanger: 60000, } as OntimeEvent; const cloned = cloneEvent(original); @@ -51,5 +53,7 @@ describe('cloneEvent()', () => { expect(cloned.colour).toBe(original.colour); expect(cloned.type).toBe(SupportedEvent.Event); expect(cloned.revision).toBe(0); + expect(cloned.timeWarning).toBe(original.timeWarning); + expect(cloned.timeDanger).toBe(original.timeDanger); }); }); diff --git a/apps/client/src/common/utils/eventsManager.ts b/apps/client/src/common/utils/eventsManager.ts index 31296a13c..cee75bf9e 100644 --- a/apps/client/src/common/utils/eventsManager.ts +++ b/apps/client/src/common/utils/eventsManager.ts @@ -28,5 +28,7 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => { colour: event.colour, after: after, revision: 0, + timeWarning: event.timeWarning, + timeDanger: event.timeDanger, }; }; diff --git a/apps/client/src/common/utils/timesManager.ts b/apps/client/src/common/utils/timesManager.ts index 4c17af915..2c9c41297 100644 --- a/apps/client/src/common/utils/timesManager.ts +++ b/apps/client/src/common/utils/timesManager.ts @@ -1,4 +1,4 @@ -export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride'; +export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride' | 'timeWarning' | 'timeDanger'; /** * @description Checks which field the value relates to diff --git a/apps/client/src/features/cuesheet/cuesheet-progress/CuesheetProgress.tsx b/apps/client/src/features/cuesheet/cuesheet-progress/CuesheetProgress.tsx index 0790d6aba..cf6440b6a 100644 --- a/apps/client/src/features/cuesheet/cuesheet-progress/CuesheetProgress.tsx +++ b/apps/client/src/features/cuesheet/cuesheet-progress/CuesheetProgress.tsx @@ -2,7 +2,7 @@ import MultiPartProgressBar from '../../../common/components/multi-part-progress import { useTimer } from '../../../common/hooks/useSocket'; import useViewSettings from '../../../common/hooks-query/useViewSettings'; -import styles from "./CuesheetProgress.module.scss" +import styles from './CuesheetProgress.module.scss'; export default function CuesheetProgress() { const { data } = useViewSettings(); @@ -14,9 +14,9 @@ export default function CuesheetProgress() { now={timer.current} complete={totalTime} normalColor={data!.normalColor} - warning={data!.warningThreshold} + warning={timer.timeWarning} warningColor={data!.warningColor} - danger={data!.dangerThreshold} + danger={timer.timeDanger} dangerColor={data!.dangerColor} className={styles.progressOverride} /> diff --git a/apps/client/src/features/event-editor/EventEditor.tsx b/apps/client/src/features/event-editor/EventEditor.tsx index f9fe8f4cc..813fa9bde 100644 --- a/apps/client/src/features/event-editor/EventEditor.tsx +++ b/apps/client/src/features/event-editor/EventEditor.tsx @@ -56,6 +56,8 @@ export default function EventEditor() { isPublic={event.isPublic} endAction={event.endAction} timerType={event.timerType} + timeWarning={event.timeWarning} + timeDanger={event.timeDanger} /> { - const { eventId, timeStart, timeEnd, duration, delay, isPublic, endAction, timerType } = props; + const { eventId, timeStart, timeEnd, duration, delay, isPublic, endAction, timerType, timeWarning, timeDanger } = + props; const { updateEvent } = useEventAction(); const [warning, setWarnings] = useState({ start: '', end: '', duration: '' }); @@ -61,7 +73,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { break; } default: { - if (field === 'timerType' || field === 'endAction') { + if (field === 'timerType' || field === 'endAction' || field === 'timeWarning' || field === 'timeDanger') { // @ts-expect-error -- not sure how to typecheck here newEventData[field as keyof OntimeEvent] = value as string; } else { @@ -83,7 +95,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { - { - { - { placeholder='Duration' warning={warning.duration} /> + +
@@ -146,11 +163,30 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { - -
); diff --git a/apps/client/src/features/modals/settings-modal/InputMillisWithString.tsx b/apps/client/src/features/modals/settings-modal/InputMillisWithString.tsx deleted file mode 100644 index 4444e5a62..000000000 --- a/apps/client/src/features/modals/settings-modal/InputMillisWithString.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useController, UseControllerProps } from 'react-hook-form'; -import { Input } from '@chakra-ui/react'; -import { ViewSettings } from 'ontime-types'; - -import { millisToMinutes } from '../../../common/utils/dateConfig'; -import { inputProps } from '../modalHelper'; - -export default function InputMillisWithString(props: UseControllerProps) { - const { name, control } = props; - const { - field: { onChange, value }, - } = useController({ - control, - name, - rules: { - pattern: { - value: /^[0-9]+$/, - message: 'Only numbers are valid', - }, - }, - }); - - return ( - - ); -} diff --git a/apps/client/src/features/modals/settings-modal/ViewSettingsForm.tsx b/apps/client/src/features/modals/settings-modal/ViewSettingsForm.tsx index 76c8806e6..0b738acd3 100644 --- a/apps/client/src/features/modals/settings-modal/ViewSettingsForm.tsx +++ b/apps/client/src/features/modals/settings-modal/ViewSettingsForm.tsx @@ -8,7 +8,6 @@ import { postViewSettings } from '../../../common/api/ontimeApi'; import { PopoverPickerRHF } from '../../../common/components/input/popover-picker/PopoverPicker'; import useInfo from '../../../common/hooks-query/useInfo'; import useViewSettings from '../../../common/hooks-query/useViewSettings'; -import { mtm } from '../../../common/utils/timeConstants'; import ModalLoader from '../modal-loader/ModalLoader'; import { inputProps } from '../modalHelper'; import ModalInput from '../ModalInput'; @@ -16,8 +15,6 @@ import ModalLink from '../ModalLink'; import ModalSplitInput from '../ModalSplitInput'; import OntimeModalFooter from '../OntimeModalFooter'; -import InputMillisWithString from './InputMillisWithString'; - import style from './SettingsModal.module.scss'; const cssOverrideDocsUrl = 'https://ontime.gitbook.io/v2/features/custom-styling'; @@ -31,7 +28,7 @@ export default function ViewSettingsForm() { handleSubmit, register, reset, - formState: { isSubmitting, isDirty, isValid, dirtyFields }, + formState: { isSubmitting, isDirty, isValid }, } = useForm({ defaultValues: data, values: data, @@ -47,19 +44,8 @@ export default function ViewSettingsForm() { }, [data, reset]); const onSubmit = async (formData: ViewSettings) => { - const parsedWarningThreshold = dirtyFields?.warningThreshold - ? // @ts-expect-error -- trust me - Number.parseInt(formData.warningThreshold) * mtm - : formData.warningThreshold; - const parsedDangerThreshold = dirtyFields?.dangerThreshold - ? // @ts-expect-error -- trust me - Number.parseInt(formData.dangerThreshold) * mtm - : formData.dangerThreshold; - const newData = { ...formData, - warningThreshold: parsedWarningThreshold, - dangerThreshold: parsedDangerThreshold, }; try { @@ -107,27 +93,13 @@ export default function ViewSettingsForm() { Timer view settings - + - - - - + - - - - +
diff --git a/apps/client/src/features/operator/status-bar/StatusBarProgress.tsx b/apps/client/src/features/operator/status-bar/StatusBarProgress.tsx index bb4f06e8f..065dc956e 100644 --- a/apps/client/src/features/operator/status-bar/StatusBarProgress.tsx +++ b/apps/client/src/features/operator/status-bar/StatusBarProgress.tsx @@ -20,9 +20,9 @@ export default function StatusBarProgress(props: StatusBarProgressProps) { now={timer.current} complete={totalTime} normalColor={viewSettings.normalColor} - warning={viewSettings.warningThreshold} + warning={timer?.timeWarning} warningColor={viewSettings.warningColor} - danger={viewSettings.dangerThreshold} + danger={timer?.timeDanger} dangerColor={viewSettings.dangerColor} className={styles.progressOverride} /> diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx index 4a4fc648a..b60fe2ac9 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx @@ -2,7 +2,7 @@ import { memo, useCallback, useState } from 'react'; import { OntimeEvent } from 'ontime-types'; import { calculateDuration, millisToString } from 'ontime-utils'; -import TimeInput from '../../../../common/components/input/time-input/TimeInput'; +import TimeInputWithButton from '../../../../common/components/input/time-input/TimeInputWithButton'; import { useEventAction } from '../../../../common/hooks/useEventAction'; import { millisToDelayString } from '../../../../common/utils/dateConfig'; import { TimeEntryField, validateEntry } from '../../../../common/utils/timesManager'; @@ -18,7 +18,7 @@ interface EventBlockTimerProps { previousEnd: number; } -type TimeActions = 'timeStart' | 'timeEnd' | 'durationOverride'; +type TimeActions = 'timeStart' | 'timeEnd' | 'durationOverride' | 'timeWarning' | 'timeDanger'; const EventBlockTimers = (props: EventBlockTimerProps) => { const { eventId, timeStart, timeEnd, duration, delay, previousEnd } = props; @@ -70,7 +70,7 @@ const EventBlockTimers = (props: EventBlockTimerProps) => { return (
- { previousEnd={previousEnd} warning={warning.start} /> - { previousEnd={previousEnd} warning={warning.end} /> -