mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 02:13:48 +00:00
Warning and danger per event (#677)
* feat: warning and danger per event --------- Co-authored-by: Fabian Posenau <fabian@fphome.de>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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<HTMLInputElement | null>(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 (
|
||||
<InputGroup size='sm' className={inputClasses}>
|
||||
<InputLeftElement className={style.inputLeft}>
|
||||
<Tooltip label={TooltipLabel} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='ontime-subtle-white'
|
||||
className={buttonClasses}
|
||||
tabIndex={-1}
|
||||
border={isDelayed ? '1px solid #E69056' : '1px solid transparent'}
|
||||
borderRight='1px solid transparent'
|
||||
borderRadius='2px 0 0 2px'
|
||||
>
|
||||
{ButtonText}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
data-testid={`time-input-${name}`}
|
||||
className={style.inputField}
|
||||
type='text'
|
||||
placeholder={placeholder}
|
||||
variant='ontime-filled'
|
||||
onFocus={handleFocus}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={onBlurHandler}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
value={value}
|
||||
maxLength={8}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</InputGroup>
|
||||
<Input
|
||||
size='sm'
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
data-testid={`time-input-${name}`}
|
||||
className={timeInputClass}
|
||||
type='text'
|
||||
placeholder={placeholder}
|
||||
variant='ontime-filled'
|
||||
onFocus={handleFocus}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={onBlurHandler}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
value={value}
|
||||
maxLength={8}
|
||||
autoComplete='off'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<InputGroup size='sm' className={inputClasses}>
|
||||
<InputLeftElement className={style.inputLeft}>
|
||||
<Tooltip label={TooltipLabel} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='ontime-subtle-white'
|
||||
className={buttonClasses}
|
||||
tabIndex={-1}
|
||||
border={isDelayed ? '1px solid #E69056' : '1px solid transparent'}
|
||||
borderRight='1px solid transparent'
|
||||
borderRadius='2px 0 0 2px'
|
||||
>
|
||||
{ButtonText}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</InputLeftElement>
|
||||
<TimeInput
|
||||
id={id}
|
||||
name={name}
|
||||
submitHandler={submitHandler}
|
||||
time={time}
|
||||
delay={delay}
|
||||
placeholder={placeholder}
|
||||
validationHandler={validationHandler}
|
||||
previousEnd={previousEnd}
|
||||
className={style.inputField}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${className}`}>
|
||||
|
||||
@@ -15,4 +15,7 @@ export type TimeManagerType = {
|
||||
|
||||
finished: boolean;
|
||||
playback: Playback;
|
||||
|
||||
timeWarning: number | null;
|
||||
timeDanger: number | null;
|
||||
};
|
||||
|
||||
@@ -4,8 +4,6 @@ export const viewsSettingsPlaceholder: ViewSettings = {
|
||||
overrideStyles: false,
|
||||
normalColor: '#ffffffcc',
|
||||
warningColor: '#FFAB33',
|
||||
warningThreshold: 120000,
|
||||
dangerColor: '#ED3333',
|
||||
dangerThreshold: 60000,
|
||||
endMessage: '',
|
||||
};
|
||||
|
||||
@@ -17,6 +17,8 @@ export const runtimeStorePlaceholder: RuntimeStore = {
|
||||
duration: null,
|
||||
timerType: null,
|
||||
endAction: null,
|
||||
timeWarning: null,
|
||||
timeDanger: null,
|
||||
},
|
||||
playback: Playback.Stop,
|
||||
timerMessage: {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -56,6 +56,8 @@ export default function EventEditor() {
|
||||
isPublic={event.isPublic}
|
||||
endAction={event.endAction}
|
||||
timerType={event.timerType}
|
||||
timeWarning={event.timeWarning}
|
||||
timeDanger={event.timeDanger}
|
||||
/>
|
||||
<EventEditorDataLeft
|
||||
key={`${event.id}-left`}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { EndAction, OntimeEvent, TimerType } 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 { cx } from '../../../common/utils/styleUtils';
|
||||
@@ -20,13 +21,24 @@ interface EventEditorTimesProps {
|
||||
isPublic: boolean;
|
||||
endAction: EndAction;
|
||||
timerType: TimerType;
|
||||
timeWarning: number;
|
||||
timeDanger: number;
|
||||
}
|
||||
|
||||
type TimeActions = 'timeStart' | 'timeEnd' | 'durationOverride' | 'timerType' | 'endAction' | 'isPublic';
|
||||
type TimeActions =
|
||||
| 'timeStart'
|
||||
| 'timeEnd'
|
||||
| 'durationOverride'
|
||||
| 'timerType'
|
||||
| 'endAction'
|
||||
| 'isPublic'
|
||||
| 'timeWarning'
|
||||
| 'timeDanger';
|
||||
|
||||
// Todo: add previous end to TimeInput fields
|
||||
const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
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) => {
|
||||
<label className={inputTimeLabels} htmlFor='timeStart'>
|
||||
{startLabel}
|
||||
</label>
|
||||
<TimeInput
|
||||
<TimeInputWithButton
|
||||
id='timeStart'
|
||||
name='timeStart'
|
||||
submitHandler={handleSubmit}
|
||||
@@ -96,7 +108,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
<label className={inputTimeLabels} htmlFor='timeEnd'>
|
||||
{endLabel}
|
||||
</label>
|
||||
<TimeInput
|
||||
<TimeInputWithButton
|
||||
id='timeEnd'
|
||||
name='timeEnd'
|
||||
submitHandler={handleSubmit}
|
||||
@@ -109,7 +121,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
<label className={style.inputLabel} htmlFor='durationOverride'>
|
||||
Duration
|
||||
</label>
|
||||
<TimeInput
|
||||
<TimeInputWithButton
|
||||
id='durationOverride'
|
||||
name='durationOverride'
|
||||
submitHandler={handleSubmit}
|
||||
@@ -118,6 +130,11 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
placeholder='Duration'
|
||||
warning={warning.duration}
|
||||
/>
|
||||
<span className={style.spacer} />
|
||||
<label className={`${style.inputLabel} ${style.publicToggle}`}>
|
||||
<Switch isChecked={isPublic} onChange={() => handleSubmit('isPublic', isPublic)} variant='ontime' />
|
||||
Event is public
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.timeSettings}>
|
||||
<label className={style.inputLabel}>Timer Type</label>
|
||||
@@ -146,11 +163,30 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
<option value={EndAction.LoadNext}>Load Next</option>
|
||||
<option value={EndAction.PlayNext}>Play Next</option>
|
||||
</Select>
|
||||
<span className={style.spacer} />
|
||||
<label className={`${style.inputLabel} ${style.publicToggle}`}>
|
||||
<Switch isChecked={isPublic} onChange={() => handleSubmit('isPublic', isPublic)} variant='ontime' />
|
||||
Event is public
|
||||
<label className={style.inputLabel} htmlFor='timeWarning'>
|
||||
Warning Time
|
||||
</label>
|
||||
<TimeInput
|
||||
id='timeWarning'
|
||||
name='timeWarning'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={timerValidationHandler}
|
||||
time={timeWarning}
|
||||
placeholder='Duration'
|
||||
warning={warning.duration}
|
||||
/>
|
||||
<label className={style.inputLabel} htmlFor='timeDanger'>
|
||||
Danger Time
|
||||
</label>
|
||||
<TimeInput
|
||||
id='timeDanger'
|
||||
name='timeDanger'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={timerValidationHandler}
|
||||
time={timeDanger}
|
||||
placeholder='Duration'
|
||||
warning={warning.duration}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<ViewSettings>) {
|
||||
const { name, control } = props;
|
||||
const {
|
||||
field: { onChange, value },
|
||||
} = useController({
|
||||
control,
|
||||
name,
|
||||
rules: {
|
||||
pattern: {
|
||||
value: /^[0-9]+$/,
|
||||
message: 'Only numbers are valid',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...inputProps}
|
||||
type='number'
|
||||
variant='ontime-filled-on-light'
|
||||
width='75px'
|
||||
size='sm'
|
||||
maxLength={3}
|
||||
defaultValue={millisToMinutes(value as number)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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<ViewSettings>({
|
||||
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() {
|
||||
<Switch {...register('overrideStyles')} variant='ontime-on-light' />
|
||||
</ModalSplitInput>
|
||||
<span className={style.title}>Timer view settings</span>
|
||||
<ModalSplitInput field='normalColor' title='Timer colour' description='Normal colour of a running timer'>
|
||||
<ModalSplitInput field='normalColor' title='Timer color' description='Normal color of a running timer'>
|
||||
<PopoverPickerRHF name='normalColor' control={control} />
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput
|
||||
field='warningColor'
|
||||
title='Warning Color'
|
||||
description='Time (in minutes) when the timer moves to warning mode'
|
||||
>
|
||||
<InputMillisWithString name='warningThreshold' control={control} />
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='warningColor' title='Warning Color' description='Colour of timer in warning mode'>
|
||||
<ModalSplitInput field='warningColor' title='Warning color' description='Color of timer in warning mode'>
|
||||
<PopoverPickerRHF name='warningColor' control={control} />
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput
|
||||
field='dangerThreshold'
|
||||
title='Danger colour'
|
||||
description='Time (in minutes) when the timer moves to danger mode'
|
||||
>
|
||||
<InputMillisWithString name='dangerThreshold' control={control} />
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput field='dangerColor' title='Timer colour' description='Colour of timer in danger mode'>
|
||||
<ModalSplitInput field='dangerColor' title='Timer color' description='Color of timer in danger mode'>
|
||||
<PopoverPickerRHF name='dangerColor' control={control} />
|
||||
</ModalSplitInput>
|
||||
<div style={{ height: '16px' }} />
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<div className={style.eventTimers}>
|
||||
<TimeInput
|
||||
<TimeInputWithButton
|
||||
name='timeStart'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={handleValidation}
|
||||
@@ -80,7 +80,7 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
|
||||
previousEnd={previousEnd}
|
||||
warning={warning.start}
|
||||
/>
|
||||
<TimeInput
|
||||
<TimeInputWithButton
|
||||
name='timeEnd'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={handleValidation}
|
||||
@@ -90,7 +90,7 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
|
||||
previousEnd={previousEnd}
|
||||
warning={warning.end}
|
||||
/>
|
||||
<TimeInput
|
||||
<TimeInputWithButton
|
||||
name='durationOverride'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={handleValidation}
|
||||
|
||||
@@ -135,8 +135,8 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
const showFinished = finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
|
||||
|
||||
const showProgress = time.playback !== Playback.Stop;
|
||||
const showWarning = (time.current ?? 1) < viewSettings.warningThreshold;
|
||||
const showDanger = (time.current ?? 1) < viewSettings.dangerThreshold;
|
||||
const showWarning = (time.current ?? 1) < (time.timeWarning ?? 0);
|
||||
const showDanger = (time.current ?? 1) < (time.timeDanger ?? 0);
|
||||
const showBlinking = pres.timerBlink;
|
||||
const showBlackout = pres.timerBlackout;
|
||||
|
||||
|
||||
@@ -98,8 +98,8 @@ export default function Timer(props: TimerProps) {
|
||||
const showEndMessage = (time.current ?? 1) < 0 && viewSettings.endMessage;
|
||||
const showProgress = time.playback !== Playback.Stop;
|
||||
const showFinished = finished && (time.timerType !== TimerType.Clock || showEndMessage);
|
||||
const showWarning = (time.current ?? 1) < viewSettings.warningThreshold;
|
||||
const showDanger = (time.current ?? 1) < viewSettings.dangerThreshold;
|
||||
const showWarning = (time.current ?? 1) < (eventNow?.timeWarning ?? 0);
|
||||
const showDanger = (time.current ?? 1) < (eventNow?.timeDanger ?? 0);
|
||||
const showBlinking = pres.timerBlink;
|
||||
const showBlackout = pres.timerBlackout;
|
||||
const showClock = time.timerType !== TimerType.Clock;
|
||||
@@ -176,9 +176,9 @@ export default function Timer(props: TimerProps) {
|
||||
now={time.current}
|
||||
complete={totalTime}
|
||||
normalColor={viewSettings.normalColor}
|
||||
warning={viewSettings.warningThreshold}
|
||||
warning={eventNow?.timeWarning}
|
||||
warningColor={viewSettings.warningColor}
|
||||
danger={viewSettings.dangerThreshold}
|
||||
danger={eventNow?.timeDanger}
|
||||
dangerColor={viewSettings.dangerColor}
|
||||
hidden={!showProgress}
|
||||
/>
|
||||
|
||||
@@ -24,9 +24,7 @@ export const dbModel: DatabaseModel = {
|
||||
overrideStyles: false,
|
||||
normalColor: '#ffffffcc',
|
||||
warningColor: '#FFAB33',
|
||||
warningThreshold: 120000,
|
||||
dangerColor: '#ED3333',
|
||||
dangerThreshold: 60000,
|
||||
endMessage: '',
|
||||
},
|
||||
aliases: [],
|
||||
|
||||
@@ -25,6 +25,8 @@ export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
};
|
||||
|
||||
export const delay: Omit<OntimeDelay, 'id'> = {
|
||||
|
||||
@@ -79,6 +79,8 @@ export class TimerService {
|
||||
duration: null,
|
||||
timerType: null,
|
||||
endAction: null,
|
||||
timeWarning: null,
|
||||
timeDanger: null,
|
||||
};
|
||||
this.loadedTimerId = null;
|
||||
this.loadedTimerStart = null;
|
||||
@@ -156,6 +158,8 @@ export class TimerService {
|
||||
this.timer.endAction = timer.endAction;
|
||||
this.loadedTimerStart = timer.timeStart;
|
||||
this.loadedTimerEnd = timer.timeEnd;
|
||||
this.timer.timeWarning = timer.timeWarning;
|
||||
this.timer.timeDanger = timer.timeDanger;
|
||||
|
||||
// this might not be ideal
|
||||
this.timer.finishedAt = null;
|
||||
@@ -196,6 +200,8 @@ export class TimerService {
|
||||
this.timer.endAction = timer.endAction;
|
||||
this.pausedTime = 0;
|
||||
this.pausedAt = 0;
|
||||
this.timer.timeWarning = timer.timeWarning;
|
||||
this.timer.timeDanger = timer.timeDanger;
|
||||
|
||||
this.timer.current = this.timer.duration;
|
||||
if (this.timer.timerType === TimerType.TimeToEnd) {
|
||||
|
||||
@@ -30,6 +30,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '659e1',
|
||||
cue: '1',
|
||||
},
|
||||
@@ -64,6 +66,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '1c48f',
|
||||
cue: '2',
|
||||
},
|
||||
@@ -98,6 +102,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: 'd48c2',
|
||||
cue: '3',
|
||||
},
|
||||
@@ -131,6 +137,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '2f185',
|
||||
cue: '4',
|
||||
},
|
||||
@@ -173,6 +181,8 @@ describe('getDelayAt()', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '659e1',
|
||||
delay: 0,
|
||||
cue: '1',
|
||||
@@ -208,6 +218,8 @@ describe('getDelayAt()', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '1c48f',
|
||||
delay: 600000,
|
||||
cue: '2',
|
||||
@@ -243,6 +255,8 @@ describe('getDelayAt()', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: 'd48c2',
|
||||
delay: 1800000,
|
||||
cue: '3',
|
||||
@@ -277,6 +291,8 @@ describe('getDelayAt()', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '2f185',
|
||||
delay: 0,
|
||||
cue: '4',
|
||||
@@ -337,6 +353,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '659e1',
|
||||
delay: 0,
|
||||
cue: '1',
|
||||
@@ -372,6 +390,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '1c48f',
|
||||
delay: 0,
|
||||
cue: '2',
|
||||
@@ -407,6 +427,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: 'd48c2',
|
||||
delay: 1800000,
|
||||
cue: '3',
|
||||
@@ -441,6 +463,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '2f185',
|
||||
delay: 0,
|
||||
cue: '4',
|
||||
|
||||
@@ -852,9 +852,7 @@ describe('test views import', () => {
|
||||
viewSettings: {
|
||||
normalColor: '#ffffffcc',
|
||||
warningColor: '#FFAB33',
|
||||
warningThreshold: 120000,
|
||||
dangerColor: '#ED3333',
|
||||
dangerThreshold: 60000,
|
||||
endMessage: '',
|
||||
overrideStyles: false,
|
||||
notAthing: true,
|
||||
@@ -866,9 +864,7 @@ describe('test views import', () => {
|
||||
const expectedParsedViewSettings = {
|
||||
normalColor: '#ffffffcc',
|
||||
warningColor: '#FFAB33',
|
||||
warningThreshold: 120000,
|
||||
dangerColor: '#ED3333',
|
||||
dangerThreshold: 60000,
|
||||
endMessage: '',
|
||||
overrideStyles: false,
|
||||
};
|
||||
|
||||
@@ -88,6 +88,8 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
let timeStartIndex: number | null = null;
|
||||
let timeEndIndex: number | null = null;
|
||||
let durationIndex: number | null = null;
|
||||
let timeWarningIndex: number | null = null;
|
||||
let timeDangerIndex: number | null = null;
|
||||
|
||||
// options: enum properties
|
||||
let endActionIndex: number | null = null;
|
||||
@@ -151,6 +153,8 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
[importMap.user7]: (index: number) => (user7Index = index),
|
||||
[importMap.user8]: (index: number) => (user8Index = index),
|
||||
[importMap.user9]: (index: number) => (user9Index = index),
|
||||
[importMap.timeWarning]: (index: number) => (timeWarningIndex = index),
|
||||
[importMap.timeDanger]: (index: number) => (timeDangerIndex = index),
|
||||
} as const;
|
||||
|
||||
row.forEach((column, j) => {
|
||||
@@ -219,6 +223,10 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
event.user8 = makeString(column, '');
|
||||
} else if (j === user9Index) {
|
||||
event.user9 = makeString(column, '');
|
||||
} else if (j === timeWarningIndex) {
|
||||
event.timeWarning = parseExcelDate(column);
|
||||
} else if (j === timeDangerIndex) {
|
||||
event.timeDanger = parseExcelDate(column);
|
||||
} else {
|
||||
// 2. if there is no flag, lets see if we know the field type
|
||||
if (typeof column === 'string') {
|
||||
@@ -333,6 +341,8 @@ export const validateEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: stri
|
||||
cue: makeString(e.cue, cueFallback),
|
||||
id,
|
||||
type: 'event',
|
||||
timeWarning: e.timeWarning,
|
||||
timeDanger: e.timeDanger,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -149,9 +149,7 @@ export const parseViewSettings = (data): ViewSettings => {
|
||||
overrideStyles: v.overrideStyles ?? dbModel.viewSettings.overrideStyles,
|
||||
normalColor: v.normalColor ?? dbModel.viewSettings.normalColor,
|
||||
warningColor: v.warningColor ?? dbModel.viewSettings.warningColor,
|
||||
warningThreshold: v.warningThreshold ?? dbModel.viewSettings.warningThreshold,
|
||||
dangerColor: v.dangerColor ?? dbModel.viewSettings.dangerColor,
|
||||
dangerThreshold: v.dangerThreshold ?? dbModel.viewSettings.dangerThreshold,
|
||||
endMessage: v.endMessage ?? dbModel.viewSettings.endMessage,
|
||||
};
|
||||
|
||||
|
||||
@@ -51,4 +51,6 @@ export type OntimeEvent = OntimeBaseEvent & {
|
||||
user9: string;
|
||||
revision: number;
|
||||
delay?: number; // calculated at runtime
|
||||
timeWarning: number;
|
||||
timeDanger: number;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,5 @@ export type ViewSettings = {
|
||||
endMessage: string;
|
||||
normalColor: string;
|
||||
warningColor: string;
|
||||
warningThreshold: number;
|
||||
dangerColor: string;
|
||||
dangerThreshold: number;
|
||||
};
|
||||
|
||||
@@ -14,4 +14,6 @@ export type TimerState = {
|
||||
duration: number | null;
|
||||
timerType: TimerType | null;
|
||||
endAction: EndAction | null;
|
||||
timeWarning: number | null;
|
||||
timeDanger: number | null;
|
||||
};
|
||||
|
||||
@@ -32,6 +32,8 @@ export const defaultExcelImportMap = {
|
||||
user7: 'user7',
|
||||
user8: 'user8',
|
||||
user9: 'user9',
|
||||
timeWarning: 'timeWarning',
|
||||
timeDanger: 'timeDanger',
|
||||
};
|
||||
|
||||
export function isExcelImportMap(obj: unknown): obj is ExcelImportMap {
|
||||
|
||||
Reference in New Issue
Block a user