mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 19:03:47 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 261e71e8c3 | |||
| 9b464ff2db | |||
| 544f01630b | |||
| 022778f892 | |||
| c9545d06e2 | |||
| c9013fc8cf | |||
| 4b49d13366 |
@@ -36,7 +36,7 @@ jobs:
|
|||||||
APPLE_TEAM_ID: ${{ secrets.TEAMID }}
|
APPLE_TEAM_ID: ${{ secrets.TEAMID }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
run: pnpm dist-mac --env-mode=loose
|
run: pnpm dist-mac
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ test-results
|
|||||||
playwright-report
|
playwright-report
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
automated-screenshots
|
automated-screenshots
|
||||||
e2e/tests/fixtures/tmp/*
|
|
||||||
|
|
||||||
# production
|
# production
|
||||||
build/
|
build/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "3.10.3",
|
"version": "3.9.5",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
<link rel="icon" type="image/png" href="/ontime-logo.png" />
|
<link rel="icon" type="image/png" href="/ontime-logo.png" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="robots" content="noindex" />
|
|
||||||
<title>ontime</title>
|
<title>ontime</title>
|
||||||
<style>
|
<style>
|
||||||
body,
|
body,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "3.10.3",
|
"version": "3.9.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,10 +1 @@
|
|||||||
{
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
"name": "",
|
|
||||||
"short_name": "",
|
|
||||||
"icons": [
|
|
||||||
{ "src": "/ontime-logo.png", "sizes": "295x295", "type": "image/png" }
|
|
||||||
],
|
|
||||||
"theme_color": "#2B5ABC",
|
|
||||||
"background_color": "#101010",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ const dbPath = `${apiEntryUrl}/db`;
|
|||||||
* HTTP request to the current DB
|
* HTTP request to the current DB
|
||||||
*/
|
*/
|
||||||
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
||||||
return axios.post(`${dbPath}/download`, { filename });
|
return axios.post(`${dbPath}/download/`, { filename });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,23 +9,32 @@ import style from './DelayIndicator.module.scss';
|
|||||||
|
|
||||||
interface DelayIndicatorProps {
|
interface DelayIndicatorProps {
|
||||||
delayValue?: number;
|
delayValue?: number;
|
||||||
tooltipPrefix?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DelayIndicator(props: DelayIndicatorProps) {
|
export default function DelayIndicator(props: DelayIndicatorProps) {
|
||||||
const { delayValue, tooltipPrefix } = props;
|
const { delayValue } = props;
|
||||||
|
|
||||||
if (typeof delayValue !== 'number' || delayValue === 0) {
|
if (typeof delayValue === 'number') {
|
||||||
return null;
|
if (delayValue < 0) {
|
||||||
|
return (
|
||||||
|
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue)}>
|
||||||
|
<span className={style.delaySymbol}>
|
||||||
|
<IoChevronDown />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayValue > 0) {
|
||||||
|
return (
|
||||||
|
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue)}>
|
||||||
|
<span className={style.delaySymbol}>
|
||||||
|
<IoChevronUp />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const delayString = tooltipPrefix
|
return null;
|
||||||
? `${tooltipPrefix} ${millisToDelayString(delayValue)}`
|
|
||||||
: millisToDelayString(delayValue);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip openDelay={tooltipDelayFast} label={delayString}>
|
|
||||||
<span className={style.delaySymbol}>{delayValue < 0 ? <IoChevronDown /> : <IoChevronUp />}</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ export default function Swatch(props: SwatchProps) {
|
|||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
onClick?.(color);
|
onClick?.(color);
|
||||||
};
|
};
|
||||||
const classes = cx([style.swatch, isSelected && style.selected, onClick && style.selectable]);
|
const classes = cx([style.swatch, isSelected ? style.selected : null, onClick ? style.selectable : null]);
|
||||||
|
|
||||||
if (!color) {
|
if (!color) {
|
||||||
return (
|
return (
|
||||||
<div className={classes} onClick={handleClick}>
|
<div className={`${classes} ${style.center}`} onClick={handleClick}>
|
||||||
<IoBan />
|
<IoBan />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import { useController, UseControllerProps } from 'react-hook-form';
|
|
||||||
import { IoEyedrop } from '@react-icons/all-files/io5/IoEyedrop';
|
|
||||||
import { ViewSettings } from 'ontime-types';
|
|
||||||
|
|
||||||
import PopoverPicker from '../../../../common/components/input/popover-picker/PopoverPicker';
|
|
||||||
import { debounce } from '../../../../common/utils/debounce';
|
|
||||||
import { cx, getAccessibleColour } from '../../../utils/styleUtils';
|
|
||||||
|
|
||||||
import style from './SwatchSelect.module.scss';
|
|
||||||
|
|
||||||
interface SwatchPickerProps {
|
|
||||||
color: string;
|
|
||||||
isSelected?: boolean;
|
|
||||||
onChange: (name: string) => void;
|
|
||||||
alwaysDisplayColor?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SwatchPicker(props: SwatchPickerProps) {
|
|
||||||
const { color, onChange, isSelected, alwaysDisplayColor } = props;
|
|
||||||
|
|
||||||
const debouncedOnChange = useCallback(
|
|
||||||
debounce((newValue: string) => {
|
|
||||||
onChange(newValue);
|
|
||||||
}, 500),
|
|
||||||
[onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const displayColor = alwaysDisplayColor || isSelected ? color : '';
|
|
||||||
const { color: iconColor } = getAccessibleColour(displayColor);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PopoverPicker color={displayColor} onChange={debouncedOnChange}>
|
|
||||||
<div
|
|
||||||
className={cx([style.swatch, isSelected && style.selected, style.selectable])}
|
|
||||||
style={{ backgroundColor: displayColor }}
|
|
||||||
>
|
|
||||||
<IoEyedrop color={iconColor} />
|
|
||||||
</div>
|
|
||||||
</PopoverPicker>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SwatchPickerRHF(props: UseControllerProps<ViewSettings>) {
|
|
||||||
const { name, control } = props;
|
|
||||||
const {
|
|
||||||
field: { onChange, value },
|
|
||||||
} = useController({ control, name });
|
|
||||||
|
|
||||||
const displayColor = typeof value === 'string' ? value : '';
|
|
||||||
const { color: iconColor } = getAccessibleColour(displayColor);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PopoverPicker color={value as string} onChange={onChange}>
|
|
||||||
<div className={cx([style.swatch, style.selectable])} style={{ backgroundColor: displayColor }}>
|
|
||||||
<IoEyedrop color={iconColor} />
|
|
||||||
</div>
|
|
||||||
</PopoverPicker>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
.list {
|
.list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem
|
||||||
}
|
}
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
aspect-ratio: 1;
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
border: 2px solid $gray-1200;
|
border: 2px solid $gray-1200;
|
||||||
color: $ui-white;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
border: 2px solid $blue-500;
|
border: 2px solid $blue-500;
|
||||||
@@ -20,9 +17,11 @@
|
|||||||
|
|
||||||
&.selectable {
|
&.selectable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 2px solid $blue-300;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import Swatch from './Swatch';
|
import Swatch from './Swatch';
|
||||||
import SwatchPicker from './SwatchPicker';
|
|
||||||
|
|
||||||
import style from './SwatchSelect.module.scss';
|
import style from './SwatchSelect.module.scss';
|
||||||
|
|
||||||
@@ -44,7 +43,6 @@ export default function SwatchSelect(props: ColourInputProps) {
|
|||||||
{colours.map((colour) => (
|
{colours.map((colour) => (
|
||||||
<Swatch key={colour} color={colour} onClick={setColour} isSelected={value === colour} />
|
<Swatch key={colour} color={colour} onClick={setColour} isSelected={value === colour} />
|
||||||
))}
|
))}
|
||||||
<SwatchPicker color={value} onChange={setColour} isSelected={!colours.includes(value)} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
.input {
|
.swatch {
|
||||||
color: $gray-200;
|
width: 24px;
|
||||||
border: 1px solid transparent;
|
height: 24px;
|
||||||
border-radius: 0 0 8px 8px;
|
border-radius: 12px;
|
||||||
background-color: $gray-1200;
|
border: 1px solid white;
|
||||||
padding: 0 0.5rem;
|
box-shadow: 0 0 0 1px $gray-300;
|
||||||
font-size: 1rem;
|
cursor: pointer;
|
||||||
height: 2rem;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&:hover {
|
transition-property: box-shadow;
|
||||||
background-color: $gray-1100;
|
transition-duration: $transition-time-action;
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
background-color: $gray-1000;
|
|
||||||
color: $gray-50;
|
|
||||||
border: 1px solid $blue-500;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.swatch:hover {
|
||||||
|
box-shadow: 0 0 0 1px $blue-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { HexAlphaColorPicker } from 'react-colorful';
|
||||||
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
import { useController, UseControllerProps } from 'react-hook-form';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@chakra-ui/react';
|
import { Popover, PopoverContent, PopoverTrigger } from '@chakra-ui/react';
|
||||||
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import style from './PopoverPicker.module.scss';
|
import style from './PopoverPicker.module.scss';
|
||||||
|
|
||||||
|
export function PopoverPickerRHF(props: UseControllerProps<ViewSettings>) {
|
||||||
|
const { name, control } = props;
|
||||||
|
const {
|
||||||
|
field: { onChange, value },
|
||||||
|
} = useController({ control, name });
|
||||||
|
|
||||||
|
return <PopoverPicker color={value as string} onChange={onChange} />;
|
||||||
|
}
|
||||||
|
|
||||||
interface PopoverPickerProps {
|
interface PopoverPickerProps {
|
||||||
color: string;
|
color: string;
|
||||||
onChange: (color: string) => void;
|
onChange: (color: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PopoverPicker(props: PropsWithChildren<PopoverPickerProps>) {
|
export default function PopoverPicker(props: PopoverPickerProps) {
|
||||||
const { color, onChange, children } = props;
|
const { color, onChange } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger>{children}</PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<PopoverContent className={style.small} style={{ borderRadius: '9px', width: 'auto' }}>
|
<div className={style.swatch} style={{ backgroundColor: color }} />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent style={{ width: 'auto' }}>
|
||||||
<HexAlphaColorPicker color={color} onChange={onChange} />
|
<HexAlphaColorPicker color={color} onChange={onChange} />
|
||||||
<HexColorInput color={color} onChange={onChange} className={style.input} prefixed />
|
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ export default function useReactiveTextInput(
|
|||||||
options?: {
|
options?: {
|
||||||
submitOnEnter?: boolean;
|
submitOnEnter?: boolean;
|
||||||
submitOnCtrlEnter?: boolean;
|
submitOnCtrlEnter?: boolean;
|
||||||
onCancelUpdate?: () => void;
|
|
||||||
allowSubmitSameValue?: boolean;
|
|
||||||
},
|
},
|
||||||
): UseReactiveTextInputReturn {
|
): UseReactiveTextInputReturn {
|
||||||
const [text, setText] = useState<string>(initialText);
|
const [text, setText] = useState<string>(initialText);
|
||||||
@@ -49,9 +47,7 @@ export default function useReactiveTextInput(
|
|||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(valueToSubmit: string) => {
|
(valueToSubmit: string) => {
|
||||||
// No need to update if it hasn't changed
|
// No need to update if it hasn't changed
|
||||||
if (valueToSubmit === initialText && !options?.allowSubmitSameValue) {
|
if (valueToSubmit !== initialText) {
|
||||||
options?.onCancelUpdate?.();
|
|
||||||
} else {
|
|
||||||
const cleanVal = valueToSubmit.trim();
|
const cleanVal = valueToSubmit.trim();
|
||||||
submitCallback(cleanVal);
|
submitCallback(cleanVal);
|
||||||
if (cleanVal !== valueToSubmit) {
|
if (cleanVal !== valueToSubmit) {
|
||||||
@@ -60,7 +56,7 @@ export default function useReactiveTextInput(
|
|||||||
}
|
}
|
||||||
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
||||||
},
|
},
|
||||||
[initialText, options, ref, submitCallback],
|
[initialText, ref, submitCallback],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,9 +66,8 @@ export default function useReactiveTextInput(
|
|||||||
const handleEscape = useCallback(() => {
|
const handleEscape = useCallback(() => {
|
||||||
// No need to update if it hasn't changed
|
// No need to update if it hasn't changed
|
||||||
setText(initialText);
|
setText(initialText);
|
||||||
options?.onCancelUpdate?.();
|
|
||||||
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
||||||
}, [initialText, options, ref]);
|
}, [initialText, ref]);
|
||||||
|
|
||||||
const keyHandler = useMemo(() => {
|
const keyHandler = useMemo(() => {
|
||||||
const hotKeys: HotkeyItem[] = [['Escape', handleEscape, { preventDefault: true }]];
|
const hotKeys: HotkeyItem[] = [['Escape', handleEscape, { preventDefault: true }]];
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
|||||||
// we dont know the values in the rundown, escalate to handler
|
// we dont know the values in the rundown, escalate to handler
|
||||||
if (newValue.startsWith('p') || newValue.startsWith('+')) {
|
if (newValue.startsWith('p') || newValue.startsWith('+')) {
|
||||||
submitHandler(name, newValue);
|
submitHandler(name, newValue);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueInMillis = parseUserTime(newValue);
|
const valueInMillis = parseUserTime(newValue);
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ $input-delayed-border-color: $ontime-delay-text;
|
|||||||
|
|
||||||
.timeInput {
|
.timeInput {
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: $label-gray;
|
|
||||||
|
|
||||||
&.delayed {
|
&.delayed {
|
||||||
border: 1px solid $input-delayed-border-color;
|
border: 1px solid $input-delayed-border-color;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
.container {
|
.container {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 25vh;
|
padding-top: 25vh;
|
||||||
|
|
||||||
background: $ui-black;
|
|
||||||
color: $ontime-color;
|
color: $ontime-color;
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import SwatchPicker from '../input/colour-input/SwatchPicker';
|
import PopoverPicker from '../input/popover-picker/PopoverPicker';
|
||||||
|
|
||||||
import style from './InlineColourPicker.module.scss';
|
import style from './InlineColourPicker.module.scss';
|
||||||
|
|
||||||
@@ -20,10 +20,13 @@ export default function InlineColourPicker(props: InlineColourPickerProps) {
|
|||||||
const { name, value } = props;
|
const { name, value } = props;
|
||||||
const [colour, setColour] = useState(() => ensureHex(value));
|
const [colour, setColour] = useState(() => ensureHex(value));
|
||||||
|
|
||||||
|
const debouncedChange = (value: string) => {
|
||||||
|
setColour(value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.inline}>
|
<div className={style.inline}>
|
||||||
<SwatchPicker color={colour} onChange={setColour} alwaysDisplayColor />
|
<PopoverPicker color={colour} onChange={debouncedChange} />
|
||||||
<span>{colour}</span>
|
|
||||||
<input type='hidden' name={name} value={colour} />
|
<input type='hidden' name={name} value={colour} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundownEntry,
|
OntimeRundownEntry,
|
||||||
RundownCached,
|
RundownCached,
|
||||||
TimeField,
|
|
||||||
TimeStrategy,
|
|
||||||
TransientEventPayload,
|
TransientEventPayload,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { dayInMs, MILLIS_PER_SECOND, parseUserTime, reorderArray, swapEventData } from 'ontime-utils';
|
import { dayInMs, MILLIS_PER_SECOND, parseUserTime, reorderArray, swapEventData } from 'ontime-utils';
|
||||||
@@ -216,75 +214,13 @@ export const useEventAction = () => {
|
|||||||
[updateEvent],
|
[updateEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type TimeField = 'timeStart' | 'timeEnd' | 'duration';
|
||||||
/**
|
/**
|
||||||
* Updates time of existing event
|
* Updates time of existing event
|
||||||
* @param eventId {string} - id of the event
|
|
||||||
* @param field {TimeField} - field to update
|
|
||||||
* @param value {string} - new value string to be parsed
|
|
||||||
* @param lockOnUpdate {boolean} - whether we will apply the lock / release on update
|
|
||||||
*/
|
*/
|
||||||
const updateTimer = useCallback(
|
const updateTimer = useCallback(
|
||||||
async (eventId: string, field: TimeField, value: string, lockOnUpdate?: boolean) => {
|
async (eventId: string, field: TimeField, value: string) => {
|
||||||
// an empty value with no lock has no domain validity
|
const getPreviousEnd = (): number => {
|
||||||
if (!lockOnUpdate && value === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newEvent: Partial<OntimeEvent> = {
|
|
||||||
id: eventId,
|
|
||||||
};
|
|
||||||
|
|
||||||
// check if we should lock the field
|
|
||||||
if (lockOnUpdate) {
|
|
||||||
if (field === 'timeEnd') {
|
|
||||||
// an empty value indicates that we should unlock the field
|
|
||||||
newEvent.timeStrategy = value === '' ? TimeStrategy.LockDuration : TimeStrategy.LockEnd;
|
|
||||||
newEvent.timeEnd = value === '' ? undefined : calculateNewValue();
|
|
||||||
} else if (field === 'duration') {
|
|
||||||
// an empty value indicates that we should unlock the field
|
|
||||||
newEvent.timeStrategy = value === '' ? TimeStrategy.LockEnd : TimeStrategy.LockDuration;
|
|
||||||
newEvent.duration = value === '' ? undefined : calculateNewValue();
|
|
||||||
} else if (field === 'timeStart') {
|
|
||||||
// an empty values means we should link to the previous
|
|
||||||
newEvent.linkStart = value === '' ? 'true' : null;
|
|
||||||
newEvent.timeStart = value === '' ? undefined : calculateNewValue();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newEvent[field] = calculateNewValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await _updateEventMutation.mutateAsync(newEvent);
|
|
||||||
} catch (error) {
|
|
||||||
logAxiosError('Error updating event', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to calculate the new time value
|
|
||||||
*/
|
|
||||||
function calculateNewValue(): number {
|
|
||||||
let newValMillis = 0;
|
|
||||||
|
|
||||||
// check for previous keyword
|
|
||||||
if (value === 'p' || value === 'prev' || value === 'previous') {
|
|
||||||
newValMillis = getPreviousEnd();
|
|
||||||
|
|
||||||
// check for adding time keyword
|
|
||||||
} else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) {
|
|
||||||
// TODO: is this logic solid?
|
|
||||||
const remainingString = value.substring(1);
|
|
||||||
newValMillis = getPreviousEnd() + parseUserTime(remainingString);
|
|
||||||
} else {
|
|
||||||
newValMillis = parseUserTime(value);
|
|
||||||
}
|
|
||||||
// dont allow timer values over 23:59:59
|
|
||||||
return Math.min(newValMillis, dayInMs - MILLIS_PER_SECOND);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to get the previous event end time
|
|
||||||
*/
|
|
||||||
function getPreviousEnd(): number {
|
|
||||||
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
|
||||||
if (!cachedRundown?.order || !cachedRundown?.rundown) {
|
if (!cachedRundown?.order || !cachedRundown?.rundown) {
|
||||||
@@ -304,6 +240,34 @@ export const useEventAction = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return previousEnd;
|
return previousEnd;
|
||||||
|
};
|
||||||
|
|
||||||
|
let newValMillis = 0;
|
||||||
|
|
||||||
|
// check for previous keyword
|
||||||
|
if (value === 'p' || value === 'prev' || value === 'previous') {
|
||||||
|
newValMillis = getPreviousEnd();
|
||||||
|
|
||||||
|
// check for adding time keyword
|
||||||
|
} else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) {
|
||||||
|
// TODO: is this logic solid?
|
||||||
|
const remainingString = value.substring(1);
|
||||||
|
newValMillis = getPreviousEnd() + parseUserTime(remainingString);
|
||||||
|
} else {
|
||||||
|
newValMillis = parseUserTime(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dont allow timer values over 23:59:59
|
||||||
|
const cappedMillis = Math.min(newValMillis, dayInMs - MILLIS_PER_SECOND);
|
||||||
|
|
||||||
|
const newEvent = {
|
||||||
|
id: eventId,
|
||||||
|
[field]: cappedMillis,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await _updateEventMutation.mutateAsync(newEvent);
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosError('Error updating event', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[_updateEventMutation, queryClient],
|
[_updateEventMutation, queryClient],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { throttle } from '../../common/utils/throttle';
|
import { debounce } from '../../common/utils/debounce';
|
||||||
|
|
||||||
export const useFadeOutOnInactivity = () => {
|
export const useFadeOutOnInactivity = () => {
|
||||||
const [isMouseMoved, setIsMouseMoved] = useState(false);
|
const [isMouseMoved, setIsMouseMoved] = useState(false);
|
||||||
@@ -16,11 +16,11 @@ export const useFadeOutOnInactivity = () => {
|
|||||||
fadeOut = setTimeout(() => setIsMouseMoved(false), 3000);
|
fadeOut = setTimeout(() => setIsMouseMoved(false), 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const throttledShowMenu = throttle(setShowMenuTrue, 1000);
|
const debouncedShowMenu = debounce(setShowMenuTrue, 1000);
|
||||||
|
|
||||||
document.addEventListener('mousemove', throttledShowMenu);
|
document.addEventListener('mousemove', debouncedShowMenu);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousemove', throttledShowMenu);
|
document.removeEventListener('mousemove', debouncedShowMenu);
|
||||||
if (fadeOut) {
|
if (fadeOut) {
|
||||||
clearTimeout(fadeOut);
|
clearTimeout(fadeOut);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const useMessagePreview = () => {
|
|||||||
showExternalMessage: state.message.timer.secondarySource === 'external' && Boolean(state.message.external),
|
showExternalMessage: state.message.timer.secondarySource === 'external' && Boolean(state.message.external),
|
||||||
showTimerMessage: state.message.timer.visible && Boolean(state.message.timer.text),
|
showTimerMessage: state.message.timer.visible && Boolean(state.message.timer.text),
|
||||||
timerType: state.eventNow?.timerType ?? null,
|
timerType: state.eventNow?.timerType ?? null,
|
||||||
countToEnd: state.eventNow?.countToEnd ?? false,
|
isTimeToEnd: state.eventNow?.isTimeToEnd ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ export type ViewExtendedTimer = {
|
|||||||
|
|
||||||
clock: number;
|
clock: number;
|
||||||
timerType: TimerType;
|
timerType: TimerType;
|
||||||
countToEnd: boolean;
|
isTimeToEnd: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
export function debounce<T extends any[]>(callback: (...args: T) => void, wait: number) {
|
export function debounce(callback: () => void, wait: number) {
|
||||||
let timeout: NodeJS.Timeout | null;
|
let timeout: NodeJS.Timeout | null;
|
||||||
return (...args: T) => {
|
return () => {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout);
|
return;
|
||||||
}
|
}
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
timeout = null;
|
timeout = null;
|
||||||
callback(...args);
|
callback();
|
||||||
}, wait);
|
}, wait);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,13 @@ export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
|
|||||||
timeEnd: event.timeEnd,
|
timeEnd: event.timeEnd,
|
||||||
timerType: event.timerType,
|
timerType: event.timerType,
|
||||||
timeStrategy: event.timeStrategy,
|
timeStrategy: event.timeStrategy,
|
||||||
countToEnd: event.countToEnd,
|
isTimeToEnd: event.isTimeToEnd,
|
||||||
linkStart: event.linkStart,
|
linkStart: event.linkStart,
|
||||||
endAction: event.endAction,
|
endAction: event.endAction,
|
||||||
isPublic: event.isPublic,
|
isPublic: event.isPublic,
|
||||||
skip: event.skip,
|
skip: event.skip,
|
||||||
colour: event.colour,
|
colour: event.colour,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: event.timeWarning,
|
timeWarning: event.timeWarning,
|
||||||
timeDanger: event.timeDanger,
|
timeDanger: event.timeDanger,
|
||||||
custom: {},
|
custom: {},
|
||||||
|
|||||||
@@ -7,11 +7,3 @@ export function isKeyEnter<T>(event: KeyboardEvent<T>): boolean {
|
|||||||
export function isKeyEscape<T>(event: KeyboardEvent<T>): boolean {
|
export function isKeyEscape<T>(event: KeyboardEvent<T>): boolean {
|
||||||
return event.key === 'Escape';
|
return event.key === 'Escape';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function preventEscape<T>(event: KeyboardEvent<T>, callback?: () => void): void {
|
|
||||||
if (isKeyEscape(event)) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
callback?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
export function throttle<T extends any[]>(callback: (...args: T) => void, wait: number) {
|
|
||||||
let timeout: NodeJS.Timeout | null;
|
|
||||||
return (...args: T) => {
|
|
||||||
if (timeout) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
timeout = null;
|
|
||||||
callback(...args);
|
|
||||||
}, wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-21
@@ -6,9 +6,6 @@ declare module '*.scss' {
|
|||||||
type ListenerType = (event: 'string', args: unknown[]) => void;
|
type ListenerType = (event: 'string', args: unknown[]) => void;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
|
||||||
* Register the electron properties
|
|
||||||
*/
|
|
||||||
interface Window {
|
interface Window {
|
||||||
ipcRenderer: {
|
ipcRenderer: {
|
||||||
send: (channel: string, args?: string | object) => void;
|
send: (channel: string, args?: string | object) => void;
|
||||||
@@ -20,24 +17,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* We pass a custom property to the table meta to allow field update
|
|
||||||
*/
|
|
||||||
declare module '@tanstack/react-table' {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
interface TableMeta<TData extends RowData> {
|
|
||||||
handleUpdate: (rowIndex: number, accessor: string, payload: string, isCustom: boolean) => void;
|
|
||||||
handleUpdateTimer: (eventId: string, field: TimeField, payload: string) => void;
|
|
||||||
options: {
|
|
||||||
showDelayedTimes: boolean;
|
|
||||||
hideTableSeconds: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allow passing CSS Properties
|
|
||||||
*/
|
|
||||||
declare module 'react' {
|
declare module 'react' {
|
||||||
interface CSSProperties {
|
interface CSSProperties {
|
||||||
[key: `--${string}`]: string | number;
|
[key: `--${string}`]: string | number;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function PanelListItem(props: PanelListItemProps) {
|
|||||||
</li>
|
</li>
|
||||||
{panel.secondary?.map((secondary) => {
|
{panel.secondary?.map((secondary) => {
|
||||||
const id = secondary.id.split('__')[1];
|
const id = secondary.id.split('__')[1];
|
||||||
const secondaryClasses = cx([style.secondary, isSelected && location === id ? style.active : null]);
|
const secondaryClasses = cx([style.secondary, location === id ? style.active : null]);
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={secondary.id}
|
key={secondary.id}
|
||||||
|
|||||||
@@ -26,24 +26,15 @@ $inner-padding: 1rem;
|
|||||||
color: $gray-300;
|
color: $gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section, .indent {
|
.section {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 2rem;
|
||||||
font-size: calc(1rem - 1px);
|
font-size: calc(1rem - 1px);
|
||||||
|
max-width: 800px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
position: relative;
|
|
||||||
margin-top: 2rem;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indent {
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: $gray-1350;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.paragraph {
|
.paragraph {
|
||||||
@@ -93,6 +84,7 @@ $inner-padding: 1rem;
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
box-shadow: 0 1px $white-10;
|
||||||
background-color: $gray-1350;
|
background-color: $gray-1350;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,40 +163,6 @@ $inner-padding: 1rem;
|
|||||||
animation: animloader 1s ease-in infinite;
|
animation: animloader 1s ease-in infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
|
||||||
background-color: $black-10;
|
|
||||||
text-align: center;
|
|
||||||
color: $muted-gray;
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inlineSiblings {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
background-color: $black-10;
|
|
||||||
text-align: center;
|
|
||||||
color: $muted-gray;
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animloader {
|
@keyframes animloader {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { HTMLAttributes, ReactNode } from 'react';
|
import { HTMLAttributes, ReactNode } from 'react';
|
||||||
import { Button } from '@chakra-ui/react';
|
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
|
||||||
|
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
@@ -24,19 +22,10 @@ type SectionProps<C extends AllowedTags> = {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
} & JSX.IntrinsicElements[C];
|
} & JSX.IntrinsicElements[C];
|
||||||
|
|
||||||
export function Section<C extends AllowedTags = 'div'>({ as, className, children, ...props }: SectionProps<C>) {
|
export function Section<C extends AllowedTags = 'div'>({ as, children, ...props }: SectionProps<C>) {
|
||||||
const Element = as ?? 'div';
|
const Element = as ?? 'div';
|
||||||
return (
|
return (
|
||||||
<Element className={cx([style.section, className])} {...(props as HTMLAttributes<HTMLElement>)}>
|
<Element className={style.section} {...(props as HTMLAttributes<HTMLElement>)}>
|
||||||
{children}
|
|
||||||
</Element>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Indent<C extends AllowedTags = 'div'>({ as, className, children, ...props }: SectionProps<C>) {
|
|
||||||
const Element = as ?? 'div';
|
|
||||||
return (
|
|
||||||
<Element className={cx([style.indent, className])} {...(props as HTMLAttributes<HTMLElement>)}>
|
|
||||||
{children}
|
{children}
|
||||||
</Element>
|
</Element>
|
||||||
);
|
);
|
||||||
@@ -63,21 +52,8 @@ export function Table({ className, children }: { className?: string; children: R
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableEmpty({ handleClick }: { handleClick: () => void }) {
|
export function ListGroup({ children }: { children: ReactNode }) {
|
||||||
return (
|
return <ul className={style.listGroup}>{children}</ul>;
|
||||||
<tr className={style.empty}>
|
|
||||||
<td colSpan={99}>
|
|
||||||
<div>No data yet</div>
|
|
||||||
<Button onClick={handleClick} variant='ontime-subtle' rightIcon={<IoAdd />} size='sm'>
|
|
||||||
New
|
|
||||||
</Button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ListGroup({ className, children }: { className?: string; children: ReactNode }) {
|
|
||||||
return <ul className={cx([style.listGroup, className])}>{children}</ul>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListItem({ children }: { children: ReactNode }) {
|
export function ListItem({ children }: { children: ReactNode }) {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default function ClientList() {
|
|||||||
)}
|
)}
|
||||||
{name}
|
{name}
|
||||||
</td>
|
</td>
|
||||||
<td className={style.pathList}>{path}</td>
|
{isCurrent ? <td /> : <td className={style.pathList}>{path}</td>}
|
||||||
<td className={style.actionButtons}>
|
<td className={style.actionButtons}>
|
||||||
<Button
|
<Button
|
||||||
size='xs'
|
size='xs'
|
||||||
|
|||||||
@@ -149,14 +149,9 @@ export default function UrlPresetsForm() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{fields.length === 0 && <Panel.TableEmpty handleClick={addNew} />}
|
|
||||||
{fields.map((preset, index) => {
|
{fields.map((preset, index) => {
|
||||||
const maybeAliasError = errors.data?.[index]?.alias?.message;
|
const maybeAliasError = errors.data?.[index]?.alias?.message;
|
||||||
const maybeUrlError = errors.data?.[index]?.pathAndParams?.message;
|
const maybeUrlError = errors.data?.[index]?.pathAndParams?.message;
|
||||||
// only saved and enabled URLs can be tested
|
|
||||||
const canTest =
|
|
||||||
preset.alias && preset.enabled && preset.pathAndParams && !maybeAliasError && !maybeUrlError;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={preset.id}>
|
<tr key={preset.id}>
|
||||||
<td className={style.fit}>
|
<td className={style.fit}>
|
||||||
@@ -195,7 +190,6 @@ export default function UrlPresetsForm() {
|
|||||||
<td className={style.flex}>
|
<td className={style.flex}>
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
size='sm'
|
size='sm'
|
||||||
isDisabled={!canTest}
|
|
||||||
clickHandler={(event) => handleLinks(event, preset.alias)}
|
clickHandler={(event) => handleLinks(event, preset.alias)}
|
||||||
tooltip='Test preset'
|
tooltip='Test preset'
|
||||||
aria-label='Test preset'
|
aria-label='Test preset'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ViewSettings } from 'ontime-types';
|
|||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import { postViewSettings } from '../../../../common/api/viewSettings';
|
import { postViewSettings } from '../../../../common/api/viewSettings';
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
import { SwatchPickerRHF } from '../../../../common/components/input/colour-input/SwatchPicker';
|
import { PopoverPickerRHF } from '../../../../common/components/input/popover-picker/PopoverPicker';
|
||||||
import useInfo from '../../../../common/hooks-query/useInfo';
|
import useInfo from '../../../../common/hooks-query/useInfo';
|
||||||
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
@@ -111,15 +111,15 @@ export default function ViewSettingsForm() {
|
|||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Timer colour' description='Default colour of a running timer' />
|
<Panel.Field title='Timer colour' description='Default colour of a running timer' />
|
||||||
<SwatchPickerRHF name='normalColor' control={control} />
|
<PopoverPickerRHF name='normalColor' control={control} />
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Warning colour' description='Colour of a running timer in warning mode' />
|
<Panel.Field title='Warning colour' description='Colour of a running timer in warning mode' />
|
||||||
<SwatchPickerRHF name='warningColor' control={control} />
|
<PopoverPickerRHF name='warningColor' control={control} />
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Danger colour' description='Colour of a running timer in danger mode' />
|
<Panel.Field title='Danger colour' description='Colour of a running timer in danger mode' />
|
||||||
<SwatchPickerRHF name='dangerColor' control={control} />
|
<PopoverPickerRHF name='dangerColor' control={control} />
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { generateId } from 'ontime-utils';
|
|||||||
|
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { isASCII, isASCIIorEmpty, isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
import { isASCII, isASCIIorEmpty, isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
||||||
import { isOntimeCloud } from '../../../../externals';
|
import { isOntimeCloud } from '../../../../externals';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
@@ -63,6 +63,13 @@ export default function OscIntegrations() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const preventEscape = (event: React.KeyboardEvent) => {
|
||||||
|
if (isKeyEscape(event)) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddNewSubscription = () => {
|
const handleAddNewSubscription = () => {
|
||||||
prepend({
|
prepend({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ export const namedImportMap = {
|
|||||||
Duration: 'duration',
|
Duration: 'duration',
|
||||||
Cue: 'cue',
|
Cue: 'cue',
|
||||||
Title: 'title',
|
Title: 'title',
|
||||||
'Count to end': 'count to end',
|
'Time to end': 'time to end',
|
||||||
'Is Public': 'public',
|
'Is Public': 'public',
|
||||||
Skip: 'skip',
|
Skip: 'skip',
|
||||||
Note: 'notes',
|
Note: 'notes',
|
||||||
@@ -48,7 +48,7 @@ export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
|
|||||||
duration: namedImportMap.Duration,
|
duration: namedImportMap.Duration,
|
||||||
cue: namedImportMap.Cue,
|
cue: namedImportMap.Cue,
|
||||||
title: namedImportMap.Title,
|
title: namedImportMap.Title,
|
||||||
countToEnd: namedImportMap['Count to end'],
|
isTimeToEnd: namedImportMap['Time to end'],
|
||||||
isPublic: namedImportMap['Is Public'],
|
isPublic: namedImportMap['Is Public'],
|
||||||
skip: namedImportMap.Skip,
|
skip: namedImportMap.Skip,
|
||||||
note: namedImportMap.Note,
|
note: namedImportMap.Note,
|
||||||
|
|||||||
+3
-3
@@ -40,7 +40,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
|||||||
<th>Duration</th>
|
<th>Duration</th>
|
||||||
<th>Warning Time</th>
|
<th>Warning Time</th>
|
||||||
<th>Danger Time</th>
|
<th>Danger Time</th>
|
||||||
<th>Count to end</th>
|
<th>Is Time to End</th>
|
||||||
<th>Is Public</th>
|
<th>Is Public</th>
|
||||||
<th>Skip</th>
|
<th>Skip</th>
|
||||||
<th>Colour</th>
|
<th>Colour</th>
|
||||||
@@ -72,7 +72,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
|||||||
}
|
}
|
||||||
eventIndex += 1;
|
eventIndex += 1;
|
||||||
const colour = event.colour ? getAccessibleColour(event.colour) : {};
|
const colour = event.colour ? getAccessibleColour(event.colour) : {};
|
||||||
const countToEnd = booleanToText(event.countToEnd);
|
const isTimeToEnd = booleanToText(event.isTimeToEnd);
|
||||||
const isPublic = booleanToText(event.isPublic);
|
const isPublic = booleanToText(event.isPublic);
|
||||||
const skip = booleanToText(event.skip);
|
const skip = booleanToText(event.skip);
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
|||||||
<td>{millisToString(event.duration)}</td>
|
<td>{millisToString(event.duration)}</td>
|
||||||
<td>{millisToString(event.timeWarning)}</td>
|
<td>{millisToString(event.timeWarning)}</td>
|
||||||
<td>{millisToString(event.timeDanger)}</td>
|
<td>{millisToString(event.timeDanger)}</td>
|
||||||
<td className={style.center}>{countToEnd && <Tag>{countToEnd}</Tag>}</td>
|
<td className={style.center}>{isTimeToEnd && <Tag>{isTimeToEnd}</Tag>}</td>
|
||||||
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
|
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
|
||||||
<td>{skip && <Tag>{skip}</Tag>}</td>
|
<td>{skip && <Tag>{skip}</Tag>}</td>
|
||||||
<td style={{ ...colour }}>{event.colour}</td>
|
<td style={{ ...colour }}>{event.colour}</td>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { Corner } from '../../editors/editor-utils/EditorUtils';
|
|||||||
import style from './MessageControl.module.scss';
|
import style from './MessageControl.module.scss';
|
||||||
|
|
||||||
export default function TimerPreview() {
|
export default function TimerPreview() {
|
||||||
const { blink, blackout, countToEnd, phase, showAuxTimer, showExternalMessage, showTimerMessage, timerType } =
|
const { blink, blackout, isTimeToEnd, phase, showAuxTimer, showExternalMessage, showTimerMessage, timerType } =
|
||||||
useMessagePreview();
|
useMessagePreview();
|
||||||
const { data } = useViewSettings();
|
const { data } = useViewSettings();
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export default function TimerPreview() {
|
|||||||
if (phase === TimerPhase.Pending) return 'Standby to start';
|
if (phase === TimerPhase.Pending) return 'Standby to start';
|
||||||
if (phase === TimerPhase.Overtime && data.endMessage) return 'Custom end message';
|
if (phase === TimerPhase.Overtime && data.endMessage) return 'Custom end message';
|
||||||
if (timerType === TimerType.Clock) return 'Clock';
|
if (timerType === TimerType.Clock) return 'Clock';
|
||||||
if (countToEnd) return 'Count to End';
|
if (isTimeToEnd) return 'Target event scheduled end';
|
||||||
return 'Timer';
|
return 'Timer';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -77,8 +77,12 @@ export default function TimerPreview() {
|
|||||||
<Tooltip label='Time type: None' openDelay={tooltipDelayMid} shouldWrapChildren>
|
<Tooltip label='Time type: None' openDelay={tooltipDelayMid} shouldWrapChildren>
|
||||||
<IoBan className={style.statusIcon} data-active={timerType === TimerType.None} />
|
<IoBan className={style.statusIcon} data-active={timerType === TimerType.None} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label={countToEnd ? 'Count to end' : 'Count duration'} openDelay={tooltipDelayMid} shouldWrapChildren>
|
<Tooltip
|
||||||
<IoFlag className={style.statusIcon} data-active={countToEnd} />
|
label={isTimeToEnd ? 'Count to schedule' : 'Count from start'}
|
||||||
|
openDelay={tooltipDelayMid}
|
||||||
|
shouldWrapChildren
|
||||||
|
>
|
||||||
|
<IoFlag className={style.statusIcon} data-active={isTimeToEnd} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
transition-property: color;
|
transition-property: color;
|
||||||
transition-duration: $transition-time-action;
|
transition-duration: $transition-time-action;
|
||||||
border-radius: $component-border-radius-full;
|
border-radius: 99px;
|
||||||
|
|
||||||
// similar styles to ontime-button-subtle
|
// similar styles to ontime-button-subtle
|
||||||
background-color: $gray-1050;
|
background-color: $gray-1050;
|
||||||
@@ -27,16 +27,13 @@
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: $title-gray;
|
color: $ui-white;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: $aux-text-size;
|
font-size: calc(1rem - 3px);
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
margin-bottom: $element-inner-spacing;
|
margin-bottom: 0.25rem;
|
||||||
max-width: max-content; // prevent label from taking entire row
|
max-width: max-content; // prevent label from taking entire row
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { HTMLAttributes, LabelHTMLAttributes } from 'react';
|
import type { LabelHTMLAttributes, ReactNode } from 'react';
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||||
import { type IconBaseProps } from '@react-icons/all-files/lib';
|
import { type IconBaseProps } from '@react-icons/all-files/lib';
|
||||||
|
|
||||||
@@ -10,13 +10,8 @@ export function Corner({ className, ...elementProps }: IconBaseProps) {
|
|||||||
return <IoArrowUp className={cx([style.corner, className])} {...elementProps} />;
|
return <IoArrowUp className={cx([style.corner, className])} {...elementProps} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Title({ children, className, ...elementProps }: HTMLAttributes<HTMLHeadingElement>) {
|
export function Title({ children }: { children: ReactNode }) {
|
||||||
const classes = cx([style.title, className]);
|
return <h3 className={style.title}>{children}</h3>;
|
||||||
return (
|
|
||||||
<h3 className={classes} {...elementProps}>
|
|
||||||
{children}
|
|
||||||
</h3>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Label({ children, className, ...elementProps }: LabelHTMLAttributes<HTMLLabelElement>) {
|
export function Label({ children, className, ...elementProps }: LabelHTMLAttributes<HTMLLabelElement>) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import useCustomFields from '../../common/hooks-query/useCustomFields';
|
|||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import { throttle } from '../../common/utils/throttle';
|
import { debounce } from '../../common/utils/debounce';
|
||||||
import { getDefaultFormat } from '../../common/utils/time';
|
import { getDefaultFormat } from '../../common/utils/time';
|
||||||
import { getPropertyValue, isStringBoolean } from '../viewers/common/viewUtils';
|
import { getPropertyValue, isStringBoolean } from '../viewers/common/viewUtils';
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export default function Operator() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const throttledHandleScroll = throttle(handleUserScroll, 1000);
|
const debouncedHandleScroll = debounce(handleUserScroll, 1000);
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (timeoutId.current) {
|
if (timeoutId.current) {
|
||||||
@@ -97,7 +97,7 @@ export default function Operator() {
|
|||||||
|
|
||||||
setShowEditPrompt(true);
|
setShowEditPrompt(true);
|
||||||
|
|
||||||
throttledHandleScroll();
|
debouncedHandleScroll();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = useCallback((event: EditEvent) => {
|
const handleEdit = useCallback((event: EditEvent) => {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
SupportedEvent,
|
SupportedEvent,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import {
|
import {
|
||||||
checkIsNextDay,
|
|
||||||
getFirstNormal,
|
getFirstNormal,
|
||||||
getLastNormal,
|
getLastNormal,
|
||||||
getNextBlockNormal,
|
getNextBlockNormal,
|
||||||
@@ -268,7 +267,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
let eventIndex = 0;
|
let eventIndex = 0;
|
||||||
// all events before the current selected are in the past
|
// all events before the current selected are in the past
|
||||||
let isPast = Boolean(featureData?.selectedEventId);
|
let isPast = Boolean(featureData?.selectedEventId);
|
||||||
let isNextDay = false;
|
|
||||||
const isEditMode = appMode === AppMode.Edit;
|
const isEditMode = appMode === AppMode.Edit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -287,7 +286,6 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
eventIndex = 0;
|
eventIndex = 0;
|
||||||
}
|
}
|
||||||
isNextDay = false;
|
|
||||||
previousEntryId = thisId;
|
previousEntryId = thisId;
|
||||||
thisId = entryId;
|
thisId = entryId;
|
||||||
if (isOntimeEvent(entry)) {
|
if (isOntimeEvent(entry)) {
|
||||||
@@ -296,9 +294,8 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
lastEvent = thisEvent;
|
lastEvent = thisEvent;
|
||||||
|
|
||||||
if (isPlayableEvent(entry)) {
|
if (isPlayableEvent(entry)) {
|
||||||
isNextDay = checkIsNextDay(entry, lastEvent);
|
// populate previous entry
|
||||||
if (isNewLatest(entry, lastEvent)) {
|
if (isNewLatest(entry.timeStart, entry.timeEnd, lastEvent?.timeStart, lastEvent?.timeEnd)) {
|
||||||
// populate previous entry
|
|
||||||
thisEvent = entry;
|
thisEvent = entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,11 +323,12 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
loaded={isLoaded}
|
loaded={isLoaded}
|
||||||
hasCursor={hasCursor}
|
hasCursor={hasCursor}
|
||||||
isNext={isNext}
|
isNext={isNext}
|
||||||
|
previousStart={lastEvent?.timeStart}
|
||||||
|
previousEnd={lastEvent?.timeEnd}
|
||||||
previousEntryId={previousEntryId}
|
previousEntryId={previousEntryId}
|
||||||
previousEventId={lastEvent?.id}
|
previousEventId={lastEvent?.id}
|
||||||
playback={isLoaded ? featureData.playback : undefined}
|
playback={isLoaded ? featureData.playback : undefined}
|
||||||
isRolling={featureData.playback === Playback.Roll}
|
isRolling={featureData.playback === Playback.Roll}
|
||||||
isNextDay={isNextDay}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ interface RundownEntryProps {
|
|||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
hasCursor: boolean;
|
hasCursor: boolean;
|
||||||
isNext: boolean;
|
isNext: boolean;
|
||||||
isNextDay: boolean;
|
previousStart?: number;
|
||||||
|
previousEnd?: number;
|
||||||
previousEntryId?: string;
|
previousEntryId?: string;
|
||||||
previousEventId?: string;
|
previousEventId?: string;
|
||||||
playback?: Playback; // we only care about this if this event is playing
|
playback?: Playback; // we only care about this if this event is playing
|
||||||
@@ -46,12 +47,13 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
loaded,
|
loaded,
|
||||||
hasCursor,
|
hasCursor,
|
||||||
isNext,
|
isNext,
|
||||||
|
previousStart,
|
||||||
|
previousEnd,
|
||||||
previousEntryId,
|
previousEntryId,
|
||||||
previousEventId,
|
previousEventId,
|
||||||
playback,
|
playback,
|
||||||
isRolling,
|
isRolling,
|
||||||
eventIndex,
|
eventIndex,
|
||||||
isNextDay,
|
|
||||||
} = props;
|
} = props;
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
|
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
|
||||||
@@ -156,13 +158,15 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
duration={data.duration}
|
duration={data.duration}
|
||||||
timeStrategy={data.timeStrategy}
|
timeStrategy={data.timeStrategy}
|
||||||
linkStart={data.linkStart}
|
linkStart={data.linkStart}
|
||||||
countToEnd={data.countToEnd}
|
isTimeToEnd={data.isTimeToEnd}
|
||||||
isPublic={data.isPublic}
|
isPublic={data.isPublic}
|
||||||
endAction={data.endAction}
|
endAction={data.endAction}
|
||||||
timerType={data.timerType}
|
timerType={data.timerType}
|
||||||
title={data.title}
|
title={data.title}
|
||||||
note={data.note}
|
note={data.note}
|
||||||
delay={data.delay ?? 0}
|
delay={data.delay ?? 0}
|
||||||
|
previousStart={previousStart}
|
||||||
|
previousEnd={previousEnd}
|
||||||
colour={data.colour}
|
colour={data.colour}
|
||||||
isPast={isPast}
|
isPast={isPast}
|
||||||
isNext={isNext}
|
isNext={isNext}
|
||||||
@@ -171,8 +175,6 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
hasCursor={hasCursor}
|
hasCursor={hasCursor}
|
||||||
playback={playback}
|
playback={playback}
|
||||||
isRolling={isRolling}
|
isRolling={isRolling}
|
||||||
gap={data.gap}
|
|
||||||
isNextDay={isNextDay}
|
|
||||||
actionHandler={actionHandler}
|
actionHandler={actionHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
.side {
|
.side {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
max-width: 45rem;
|
max-width: 45rem;
|
||||||
margin: 0.5rem 0;
|
margin: 2rem 0;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
background-color: $gray-1325;
|
background-color: $gray-1325;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ interface EventBlockProps {
|
|||||||
duration: number;
|
duration: number;
|
||||||
timeStrategy: TimeStrategy;
|
timeStrategy: TimeStrategy;
|
||||||
linkStart: MaybeString;
|
linkStart: MaybeString;
|
||||||
countToEnd: boolean;
|
isTimeToEnd: boolean;
|
||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
endAction: EndAction;
|
endAction: EndAction;
|
||||||
@@ -39,6 +39,8 @@ interface EventBlockProps {
|
|||||||
title: string;
|
title: string;
|
||||||
note: string;
|
note: string;
|
||||||
delay: number;
|
delay: number;
|
||||||
|
previousStart?: number;
|
||||||
|
previousEnd?: number;
|
||||||
colour: string;
|
colour: string;
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
isNext: boolean;
|
isNext: boolean;
|
||||||
@@ -47,8 +49,6 @@ interface EventBlockProps {
|
|||||||
hasCursor: boolean;
|
hasCursor: boolean;
|
||||||
playback?: Playback;
|
playback?: Playback;
|
||||||
isRolling: boolean;
|
isRolling: boolean;
|
||||||
gap: number;
|
|
||||||
isNextDay: boolean;
|
|
||||||
actionHandler: (
|
actionHandler: (
|
||||||
action: EventItemActions,
|
action: EventItemActions,
|
||||||
payload?:
|
payload?:
|
||||||
@@ -69,7 +69,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
duration,
|
duration,
|
||||||
timeStrategy,
|
timeStrategy,
|
||||||
linkStart,
|
linkStart,
|
||||||
countToEnd,
|
isTimeToEnd,
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
eventIndex,
|
eventIndex,
|
||||||
endAction,
|
endAction,
|
||||||
@@ -77,6 +77,8 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
title,
|
title,
|
||||||
note,
|
note,
|
||||||
delay,
|
delay,
|
||||||
|
previousStart,
|
||||||
|
previousEnd,
|
||||||
colour,
|
colour,
|
||||||
isPast,
|
isPast,
|
||||||
isNext,
|
isNext,
|
||||||
@@ -85,8 +87,6 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
hasCursor,
|
hasCursor,
|
||||||
playback,
|
playback,
|
||||||
isRolling,
|
isRolling,
|
||||||
gap,
|
|
||||||
isNextDay,
|
|
||||||
actionHandler,
|
actionHandler,
|
||||||
} = props;
|
} = props;
|
||||||
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
||||||
@@ -273,7 +273,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
id='event-block'
|
id='event-block'
|
||||||
>
|
>
|
||||||
<RundownIndicators timeStart={timeStart} delay={delay} gap={gap} isNextDay={isNextDay} />
|
<RundownIndicators timeStart={timeStart} previousStart={previousStart} previousEnd={previousEnd} delay={delay} />
|
||||||
|
|
||||||
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
|
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
|
||||||
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
||||||
@@ -288,7 +288,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
timeEnd={timeEnd}
|
timeEnd={timeEnd}
|
||||||
duration={duration}
|
duration={duration}
|
||||||
linkStart={linkStart}
|
linkStart={linkStart}
|
||||||
countToEnd={countToEnd}
|
isTimeToEnd={isTimeToEnd}
|
||||||
timeStrategy={timeStrategy}
|
timeStrategy={timeStrategy}
|
||||||
eventId={eventId}
|
eventId={eventId}
|
||||||
eventIndex={eventIndex}
|
eventIndex={eventIndex}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { millisToString, removeTrailingZero } from 'ontime-utils';
|
import {
|
||||||
|
calculateDuration,
|
||||||
|
checkIsNextDay,
|
||||||
|
dayInMs,
|
||||||
|
getTimeFromPrevious,
|
||||||
|
millisToString,
|
||||||
|
removeTrailingZero,
|
||||||
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { formatDuration } from '../../../common/utils/time';
|
import { formatDuration } from '../../../common/utils/time';
|
||||||
|
|
||||||
@@ -10,15 +17,25 @@ export function formatDelay(timeStart: number, delay: number): string | undefine
|
|||||||
const timeTag = removeTrailingZero(millisToString(delayedStart));
|
const timeTag = removeTrailingZero(millisToString(delayedStart));
|
||||||
return `New start ${timeTag}`;
|
return `New start ${timeTag}`;
|
||||||
}
|
}
|
||||||
export function formatGap(gap: number, isNextDay: boolean) {
|
|
||||||
if (gap === 0) {
|
export function formatOverlap(timeStart: number, previousStart?: number, previousEnd?: number): string | undefined {
|
||||||
if (isNextDay) {
|
const noPreviousElement = previousEnd === undefined || previousStart === undefined;
|
||||||
// We show a next day warning even if there is no gap
|
if (noPreviousElement) return;
|
||||||
return '(next day)';
|
|
||||||
}
|
const normalisedDuration = calculateDuration(previousStart, previousEnd);
|
||||||
return;
|
const timeFromPrevious = getTimeFromPrevious(timeStart, previousStart, previousEnd, normalisedDuration);
|
||||||
|
if (timeFromPrevious === 0) return;
|
||||||
|
|
||||||
|
if (checkIsNextDay(previousStart, timeStart, normalisedDuration)) {
|
||||||
|
const previousCrossMidnight = previousStart > previousEnd;
|
||||||
|
const normalisedPreviousEnd = previousCrossMidnight ? previousEnd + dayInMs : previousEnd;
|
||||||
|
|
||||||
|
const gap = dayInMs - normalisedPreviousEnd + timeStart;
|
||||||
|
if (gap === 0) return;
|
||||||
|
const gapString = formatDuration(Math.abs(gap), false);
|
||||||
|
return `Gap ${gapString} (next day)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gapString = formatDuration(Math.abs(gap), false);
|
const overlapString = formatDuration(Math.abs(timeFromPrevious), false);
|
||||||
return `${gap < 0 ? 'Overlap' : 'Gap'} ${gapString}${isNextDay ? ' (next day)' : ''}`;
|
return `${timeFromPrevious < 0 ? 'Overlap' : 'Gap'} ${overlapString}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ interface EventBlockInnerProps {
|
|||||||
duration: number;
|
duration: number;
|
||||||
timeStrategy: TimeStrategy;
|
timeStrategy: TimeStrategy;
|
||||||
linkStart: MaybeString;
|
linkStart: MaybeString;
|
||||||
countToEnd: boolean;
|
isTimeToEnd: boolean;
|
||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
endAction: EndAction;
|
endAction: EndAction;
|
||||||
@@ -52,7 +52,7 @@ function EventBlockInner(props: EventBlockInnerProps) {
|
|||||||
duration,
|
duration,
|
||||||
timeStrategy,
|
timeStrategy,
|
||||||
linkStart,
|
linkStart,
|
||||||
countToEnd,
|
isTimeToEnd,
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
endAction,
|
endAction,
|
||||||
timerType,
|
timerType,
|
||||||
@@ -93,7 +93,7 @@ function EventBlockInner(props: EventBlockInnerProps) {
|
|||||||
delay={delay}
|
delay={delay}
|
||||||
timeStrategy={timeStrategy}
|
timeStrategy={timeStrategy}
|
||||||
linkStart={linkStart}
|
linkStart={linkStart}
|
||||||
countToEnd={countToEnd}
|
isTimeToEnd={isTimeToEnd}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.titleSection}>
|
<div className={style.titleSection}>
|
||||||
@@ -124,9 +124,9 @@ function EventBlockInner(props: EventBlockInnerProps) {
|
|||||||
<EndActionIcon action={endAction} className={style.statusIcon} />
|
<EndActionIcon action={endAction} className={style.statusIcon} />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label={`${countToEnd ? 'Count to End' : 'Count duration'}`} openDelay={tooltipDelayMid}>
|
<Tooltip label={`${isTimeToEnd ? 'Time to End' : 'Count from start'}`} openDelay={tooltipDelayMid}>
|
||||||
<span>
|
<span>
|
||||||
<IoFlag className={`${style.statusIcon} ${countToEnd ? style.active : style.disabled}`} />
|
<IoFlag className={`${style.statusIcon} ${isTimeToEnd ? style.active : style.disabled}`} />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} openDelay={tooltipDelayMid}>
|
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} openDelay={tooltipDelayMid}>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { formatDelay, formatGap } from './EventBlock.utils';
|
import { formatDelay, formatOverlap } from './EventBlock.utils';
|
||||||
|
|
||||||
import style from './RundownIndicators.module.scss';
|
import style from './RundownIndicators.module.scss';
|
||||||
|
|
||||||
interface RundownIndicatorProps {
|
interface RundownIndicatorProps {
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
isNextDay: boolean;
|
previousStart?: number;
|
||||||
|
previousEnd?: number;
|
||||||
delay: number;
|
delay: number;
|
||||||
gap: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RundownIndicators(props: RundownIndicatorProps) {
|
export default function RundownIndicators(props: RundownIndicatorProps) {
|
||||||
const { timeStart, delay, gap, isNextDay } = props;
|
const { timeStart, previousStart, previousEnd, delay } = props;
|
||||||
|
|
||||||
const hasGap = formatGap(gap, isNextDay);
|
const hasOverlap = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
const hasDelay = formatDelay(timeStart, delay);
|
const hasDelay = formatDelay(timeStart, delay);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.indicators}>
|
<div className={style.indicators}>
|
||||||
{hasDelay && <div className={style.delay}>{hasDelay}</div>}
|
{hasDelay && <div className={style.delay}>{hasDelay}</div>}
|
||||||
{hasGap && <div className={style.gap}>{hasGap}</div>}
|
{hasOverlap && <div className={style.gap}>{hasOverlap}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { formatDelay } from '../EventBlock.utils';
|
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { formatDelay, formatOverlap } from '../EventBlock.utils';
|
||||||
|
|
||||||
describe('formatDelay()', () => {
|
describe('formatDelay()', () => {
|
||||||
it('adds a given delay to the start time', () => {
|
it('adds a given delay to the start time', () => {
|
||||||
@@ -8,3 +10,69 @@ describe('formatDelay()', () => {
|
|||||||
expect(result).toEqual('New start 00:02');
|
expect(result).toEqual('New start 00:02');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('formatOverlap()', () => {
|
||||||
|
it('recognises an overlap between two times', () => {
|
||||||
|
const previousStart = 0;
|
||||||
|
const previousEnd = 60000; // 1 min
|
||||||
|
const timeStart = 30000; // 30 sec
|
||||||
|
const result = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
|
expect(result).toEqual('Overlap 30s');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bug #949 recognises an overlap between two times', () => {
|
||||||
|
const previousStart = 46800000; // 13:00:00
|
||||||
|
const previousEnd = 48600000; // 13:30:00
|
||||||
|
const timeStart = 48300000; // 13:25:00
|
||||||
|
const result = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
|
expect(result).toEqual('Overlap 5m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles events the day after, without overlap', () => {
|
||||||
|
const previousStart = 11 * MILLIS_PER_HOUR;
|
||||||
|
const previousEnd = 12 * MILLIS_PER_HOUR;
|
||||||
|
const timeStart = 6 * MILLIS_PER_HOUR;
|
||||||
|
const result = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
|
expect(result).toBe('Gap 18h (next day)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles events the day after, with gap', () => {
|
||||||
|
const previousStart = 17 * MILLIS_PER_HOUR;
|
||||||
|
const previousEnd = 23 * MILLIS_PER_HOUR;
|
||||||
|
const timeStart = 9 * MILLIS_PER_HOUR;
|
||||||
|
const result = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
|
expect(result).toBe('Gap 10h (next day)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles events the day after, with previous ending at midnight', () => {
|
||||||
|
const previousStart = 23 * MILLIS_PER_HOUR; // 23:00:00
|
||||||
|
const previousEnd = 0; // 00:00:00
|
||||||
|
const timeStart = 1 * MILLIS_PER_HOUR; // 01:00:00
|
||||||
|
const result = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
|
expect(result).toBe('Gap 1h (next day)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles sequential events the day after, with previous ending over midnight', () => {
|
||||||
|
const previousStart = 23 * MILLIS_PER_HOUR;
|
||||||
|
const previousEnd = 1 * MILLIS_PER_HOUR;
|
||||||
|
const timeStart = 1 * MILLIS_PER_HOUR;
|
||||||
|
const result = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles events the day after, with previous ending over midnight with overlap', () => {
|
||||||
|
const previousStart = 23 * MILLIS_PER_HOUR;
|
||||||
|
const previousEnd = 2 * MILLIS_PER_HOUR;
|
||||||
|
const timeStart = 1 * MILLIS_PER_HOUR;
|
||||||
|
const result = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
|
expect(result).toBe('Overlap 1h');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles events the day after, with previous ending over midnight with gap', () => {
|
||||||
|
const previousStart = 23 * MILLIS_PER_HOUR;
|
||||||
|
const previousEnd = 1 * MILLIS_PER_HOUR;
|
||||||
|
const timeStart = 2 * MILLIS_PER_HOUR;
|
||||||
|
const result = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
|
expect(result).toBe('Gap 1h');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
.eventEditor {
|
.eventEditor {
|
||||||
|
color: $label-gray;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-inline: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,22 +30,19 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
h3 {
|
|
||||||
margin-bottom: -0.5rem; // bring the title closer to the section elements
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.decorated {
|
.decorated {
|
||||||
color: var(--decorator-color, $ui-white);
|
color: var(--decorator-color, $ui-white);
|
||||||
background-color: var(--decorator-bg, $gray-1100);
|
background-color: var(--decorator-bg, $gray-1100);
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding-inline: 0.5rem;
|
padding: 0 0.5rem;
|
||||||
border-radius: $component-border-radius-sm;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delayLabel {
|
.delayLabel {
|
||||||
font-size: $aux-text-size;
|
font-size: calc(1rem - 3px);
|
||||||
color: $ontime-delay-text;
|
color: $ontime-delay-text;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
@@ -58,8 +56,7 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
max-width: max-content;
|
max-width: max-content;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 2rem; // manually match the height of a text input
|
height: 30px; // manually match the height of a text input
|
||||||
margin-bottom: 0; // reset margin from label component
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline {
|
.inline {
|
||||||
@@ -71,13 +68,6 @@
|
|||||||
.splitTwo {
|
.splitTwo {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
column-gap: 1rem;
|
column-gap: 1.5rem;
|
||||||
row-gap: 1rem;
|
row-gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltipIcon {
|
|
||||||
color: $blue-500;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 1.25em;
|
|
||||||
margin-left: 0.25em;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function EventEditor(props: EventEditorProps) {
|
|||||||
duration={event.duration}
|
duration={event.duration}
|
||||||
timeStrategy={event.timeStrategy}
|
timeStrategy={event.timeStrategy}
|
||||||
linkStart={event.linkStart}
|
linkStart={event.linkStart}
|
||||||
countToEnd={event.countToEnd}
|
isTimeToEnd={event.isTimeToEnd}
|
||||||
delay={event.delay ?? 0}
|
delay={event.delay ?? 0}
|
||||||
isPublic={event.isPublic}
|
isPublic={event.isPublic}
|
||||||
endAction={event.endAction}
|
endAction={event.endAction}
|
||||||
@@ -79,14 +79,14 @@ export default function EventEditor(props: EventEditorProps) {
|
|||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<Editor.Title>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
Custom Fields
|
<Editor.Title>Custom Fields</Editor.Title>
|
||||||
{isEditor && (
|
{isEditor && (
|
||||||
<Button variant='ontime-subtle' size='sm' onClick={handleOpenCustomManager}>
|
<Button variant='ontime-subtle' size='sm' onClick={handleOpenCustomManager}>
|
||||||
Manage
|
Manage
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Editor.Title>
|
</div>
|
||||||
{Object.keys(customFields).map((fieldKey) => {
|
{Object.keys(customFields).map((fieldKey) => {
|
||||||
const key = `${event.id}-${fieldKey}`;
|
const key = `${event.id}-${fieldKey}`;
|
||||||
const fieldName = `custom-${fieldKey}`;
|
const fieldName = `custom-${fieldKey}`;
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prompt {
|
.prompt {
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: left;
|
||||||
margin-left: 4rem;
|
margin-left: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { memo, PropsWithChildren } from 'react';
|
import { memo } from 'react';
|
||||||
import { Kbd } from '@chakra-ui/react';
|
import { Kbd } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { deviceAlt, deviceMod } from '../../../common/utils/deviceUtils';
|
import { deviceAlt, deviceMod } from '../../../common/utils/deviceUtils';
|
||||||
import * as Editor from '../../editors/editor-utils/EditorUtils';
|
|
||||||
|
|
||||||
import style from './EventEditorEmpty.module.scss';
|
import style from './EventEditorEmpty.module.scss';
|
||||||
|
|
||||||
@@ -12,7 +11,7 @@ function EventEditorEmpty() {
|
|||||||
return (
|
return (
|
||||||
<div className={style.eventEditor} data-testid='editor-container'>
|
<div className={style.eventEditor} data-testid='editor-container'>
|
||||||
<div className={style.shortcutSection}>
|
<div className={style.shortcutSection}>
|
||||||
<Editor.Title className={style.prompt}>Rundown shortcuts</Editor.Title>
|
<div className={style.prompt}>Rundown shortcuts:</div>
|
||||||
<table className={style.shortcuts}>
|
<table className={style.shortcuts}>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -169,6 +168,6 @@ function EventEditorEmpty() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuxKey({ children }: PropsWithChildren) {
|
function AuxKey({ children }: { children: React.ReactNode }) {
|
||||||
return <span className={style.divider}>{children}</span>;
|
return <span className={style.divider}>{children}</span>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Select, Switch, Tooltip } from '@chakra-ui/react';
|
import { Select, Switch } from '@chakra-ui/react';
|
||||||
import { IoInformationCircle } from '@react-icons/all-files/io5/IoInformationCircle';
|
|
||||||
import { EndAction, MaybeString, TimerType, TimeStrategy } from 'ontime-types';
|
import { EndAction, MaybeString, TimerType, TimeStrategy } from 'ontime-types';
|
||||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ interface EventEditorTimesProps {
|
|||||||
duration: number;
|
duration: number;
|
||||||
timeStrategy: TimeStrategy;
|
timeStrategy: TimeStrategy;
|
||||||
linkStart: MaybeString;
|
linkStart: MaybeString;
|
||||||
countToEnd: boolean;
|
isTimeToEnd: boolean;
|
||||||
delay: number;
|
delay: number;
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
endAction: EndAction;
|
endAction: EndAction;
|
||||||
@@ -28,7 +27,7 @@ interface EventEditorTimesProps {
|
|||||||
timeDanger: number;
|
timeDanger: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HandledActions = 'countToEnd' | 'timerType' | 'endAction' | 'isPublic' | 'timeWarning' | 'timeDanger';
|
type HandledActions = 'isTimeToEnd' | 'timerType' | 'endAction' | 'isPublic' | 'timeWarning' | 'timeDanger';
|
||||||
|
|
||||||
function EventEditorTimes(props: EventEditorTimesProps) {
|
function EventEditorTimes(props: EventEditorTimesProps) {
|
||||||
const {
|
const {
|
||||||
@@ -38,7 +37,7 @@ function EventEditorTimes(props: EventEditorTimesProps) {
|
|||||||
duration,
|
duration,
|
||||||
timeStrategy,
|
timeStrategy,
|
||||||
linkStart,
|
linkStart,
|
||||||
countToEnd,
|
isTimeToEnd,
|
||||||
delay,
|
delay,
|
||||||
isPublic,
|
isPublic,
|
||||||
endAction,
|
endAction,
|
||||||
@@ -54,8 +53,8 @@ function EventEditorTimes(props: EventEditorTimesProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field === 'countToEnd') {
|
if (field === 'isTimeToEnd') {
|
||||||
updateEvent({ id: eventId, countToEnd: !(value as boolean) });
|
updateEvent({ id: eventId, isTimeToEnd: !(value as boolean) });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +80,8 @@ function EventEditorTimes(props: EventEditorTimesProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<Editor.Title>Event schedule</Editor.Title>
|
|
||||||
<div>
|
<div>
|
||||||
|
<Editor.Label>Event schedule</Editor.Label>
|
||||||
<div className={style.inline}>
|
<div className={style.inline}>
|
||||||
<TimeInputFlow
|
<TimeInputFlow
|
||||||
eventId={eventId}
|
eventId={eventId}
|
||||||
@@ -92,16 +91,13 @@ function EventEditorTimes(props: EventEditorTimesProps) {
|
|||||||
timeStrategy={timeStrategy}
|
timeStrategy={timeStrategy}
|
||||||
linkStart={linkStart}
|
linkStart={linkStart}
|
||||||
delay={delay}
|
delay={delay}
|
||||||
countToEnd={countToEnd}
|
isTimeToEnd={isTimeToEnd}
|
||||||
showLabels
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.delayLabel}>{delayLabel}</div>
|
<div className={style.delayLabel}>{delayLabel}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={style.column}>
|
<Editor.Title>Event behaviour</Editor.Title>
|
||||||
<Editor.Title>Event Behaviour</Editor.Title>
|
|
||||||
<div className={style.splitTwo}>
|
<div className={style.splitTwo}>
|
||||||
<div>
|
<div>
|
||||||
<Editor.Label htmlFor='endAction'>End Action</Editor.Label>
|
<Editor.Label htmlFor='endAction'>End Action</Editor.Label>
|
||||||
@@ -120,30 +116,22 @@ function EventEditorTimes(props: EventEditorTimesProps) {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Editor.Label htmlFor='countToEnd'>Count to End</Editor.Label>
|
<Editor.Label htmlFor='timeToEnd'>Target Event Scheduled End</Editor.Label>
|
||||||
<Editor.Label className={style.switchLabel}>
|
<Editor.Label className={style.switchLabel}>
|
||||||
<Switch
|
<Switch
|
||||||
id='countToEnd'
|
id='timeToEnd'
|
||||||
size='md'
|
size='md'
|
||||||
isChecked={countToEnd}
|
isChecked={isTimeToEnd}
|
||||||
onChange={() => handleSubmit('countToEnd', countToEnd)}
|
onChange={() => handleSubmit('isTimeToEnd', isTimeToEnd)}
|
||||||
variant='ontime'
|
variant='ontime'
|
||||||
/>
|
/>
|
||||||
{countToEnd ? 'On' : 'Off'}
|
{isTimeToEnd ? 'On' : 'Off'}
|
||||||
</Editor.Label>
|
</Editor.Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<Editor.Title>
|
<Editor.Title>Display options</Editor.Title>
|
||||||
<Tooltip label='Changes how the timer is displayed in different views. It is not reflected in the rundown'>
|
|
||||||
<span>
|
|
||||||
Display Options
|
|
||||||
<IoInformationCircle className={style.tooltipIcon} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Editor.Title>
|
|
||||||
<div className={style.splitTwo}>
|
<div className={style.splitTwo}>
|
||||||
<div>
|
<div>
|
||||||
<Editor.Label htmlFor='timerType'>Timer Type</Editor.Label>
|
<Editor.Label htmlFor='timerType'>Timer Type</Editor.Label>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const EventEditorTitles = (props: EventEditorTitlesProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<Editor.Title>Event Data</Editor.Title>
|
<Editor.Title>Event data</Editor.Title>
|
||||||
<div className={style.splitTwo}>
|
<div className={style.splitTwo}>
|
||||||
<div>
|
<div>
|
||||||
<Editor.Label htmlFor='eventId'>Event ID (read only)</Editor.Label>
|
<Editor.Label htmlFor='eventId'>Event ID (read only)</Editor.Label>
|
||||||
|
|||||||
@@ -5,35 +5,34 @@ import { IoLink } from '@react-icons/all-files/io5/IoLink';
|
|||||||
import { IoLockClosed } from '@react-icons/all-files/io5/IoLockClosed';
|
import { IoLockClosed } from '@react-icons/all-files/io5/IoLockClosed';
|
||||||
import { IoLockOpenOutline } from '@react-icons/all-files/io5/IoLockOpenOutline';
|
import { IoLockOpenOutline } from '@react-icons/all-files/io5/IoLockOpenOutline';
|
||||||
import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink';
|
import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink';
|
||||||
import { MaybeString, TimeField, TimeStrategy } from 'ontime-types';
|
import { MaybeString, TimeStrategy } from 'ontime-types';
|
||||||
import { dayInMs } from 'ontime-utils';
|
|
||||||
|
|
||||||
import TimeInputWithButton from '../../../common/components/input/time-input/TimeInputWithButton';
|
import TimeInputWithButton from '../../../common/components/input/time-input/TimeInputWithButton';
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
import { tooltipDelayFast, tooltipDelayMid } from '../../../ontimeConfig';
|
import { tooltipDelayFast, tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
import * as Editor from '../../editors/editor-utils/EditorUtils';
|
|
||||||
|
|
||||||
import style from './TimeInputFlow.module.scss';
|
import style from './TimeInputFlow.module.scss';
|
||||||
|
|
||||||
interface EventBlockTimerProps {
|
interface EventBlockTimerProps {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
countToEnd: boolean;
|
isTimeToEnd: boolean;
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
timeEnd: number;
|
timeEnd: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
timeStrategy: TimeStrategy;
|
timeStrategy: TimeStrategy;
|
||||||
linkStart: MaybeString;
|
linkStart: MaybeString;
|
||||||
delay: number;
|
delay: number;
|
||||||
showLabels?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TimeActions = 'timeStart' | 'timeEnd' | 'duration';
|
||||||
|
|
||||||
function TimeInputFlow(props: EventBlockTimerProps) {
|
function TimeInputFlow(props: EventBlockTimerProps) {
|
||||||
const { eventId, countToEnd, timeStart, timeEnd, duration, timeStrategy, linkStart, delay, showLabels } = props;
|
const { eventId, isTimeToEnd, timeStart, timeEnd, duration, timeStrategy, linkStart, delay } = props;
|
||||||
const { updateEvent, updateTimer } = useEventAction();
|
const { updateEvent, updateTimer } = useEventAction();
|
||||||
|
|
||||||
// In sync with EventEditorTimes
|
// In sync with EventEditorTimes
|
||||||
const handleSubmit = (field: TimeField, value: string) => {
|
const handleSubmit = (field: TimeActions, value: string) => {
|
||||||
updateTimer(eventId, field, value);
|
updateTimer(eventId, field, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,12 +45,12 @@ function TimeInputFlow(props: EventBlockTimerProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
if (timeStart + duration > dayInMs) {
|
if (timeStart > timeEnd) {
|
||||||
warnings.push('Over midnight');
|
warnings.push('Over midnight');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (countToEnd) {
|
if (isTimeToEnd) {
|
||||||
warnings.push('Count to End');
|
warnings.push('Target event scheduled end');
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasDelay = delay !== 0;
|
const hasDelay = delay !== 0;
|
||||||
@@ -65,72 +64,63 @@ function TimeInputFlow(props: EventBlockTimerProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<TimeInputWithButton<TimeActions>
|
||||||
{showLabels && <Editor.Label className={style.sectionTitle}>Start time</Editor.Label>}
|
name='timeStart'
|
||||||
<TimeInputWithButton<TimeField>
|
submitHandler={handleSubmit}
|
||||||
name='timeStart'
|
time={timeStart}
|
||||||
submitHandler={handleSubmit}
|
hasDelay={hasDelay}
|
||||||
time={timeStart}
|
placeholder='Start'
|
||||||
hasDelay={hasDelay}
|
disabled={Boolean(linkStart)}
|
||||||
placeholder='Start'
|
>
|
||||||
disabled={Boolean(linkStart)}
|
<Tooltip label='Link start to previous end' openDelay={tooltipDelayMid}>
|
||||||
>
|
<InputRightElement className={activeStart} onClick={() => handleLink(!linkStart)}>
|
||||||
<Tooltip label='Link start to previous end' openDelay={tooltipDelayMid}>
|
<span className={style.timeLabel}>S</span>
|
||||||
<InputRightElement className={activeStart} onClick={() => handleLink(!linkStart)}>
|
<span className={style.fourtyfive}>{linkStart ? <IoLink /> : <IoUnlink />}</span>
|
||||||
<span className={style.timeLabel}>S</span>
|
</InputRightElement>
|
||||||
<span className={style.fourtyfive}>{linkStart ? <IoLink /> : <IoUnlink />}</span>
|
</Tooltip>
|
||||||
</InputRightElement>
|
</TimeInputWithButton>
|
||||||
</Tooltip>
|
|
||||||
</TimeInputWithButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<TimeInputWithButton<TimeActions>
|
||||||
{showLabels && <Editor.Label>End time</Editor.Label>}
|
name='timeEnd'
|
||||||
<TimeInputWithButton<TimeField>
|
submitHandler={handleSubmit}
|
||||||
name='timeEnd'
|
time={timeEnd}
|
||||||
submitHandler={handleSubmit}
|
hasDelay={hasDelay}
|
||||||
time={timeEnd}
|
disabled={isLockedDuration}
|
||||||
hasDelay={hasDelay}
|
placeholder='End'
|
||||||
disabled={isLockedDuration}
|
>
|
||||||
placeholder='End'
|
<Tooltip label='Lock end' openDelay={tooltipDelayMid}>
|
||||||
>
|
<InputRightElement
|
||||||
<Tooltip label='Lock end' openDelay={tooltipDelayMid}>
|
className={activeEnd}
|
||||||
<InputRightElement
|
onClick={() => handleChangeStrategy(TimeStrategy.LockEnd)}
|
||||||
className={activeEnd}
|
data-testid='lock__end'
|
||||||
onClick={() => handleChangeStrategy(TimeStrategy.LockEnd)}
|
>
|
||||||
data-testid='lock__end'
|
<span className={style.timeLabel}>E</span>
|
||||||
>
|
{isLockedEnd ? <IoLockClosed /> : <IoLockOpenOutline />}
|
||||||
<span className={style.timeLabel}>E</span>
|
</InputRightElement>
|
||||||
{isLockedEnd ? <IoLockClosed /> : <IoLockOpenOutline />}
|
</Tooltip>
|
||||||
</InputRightElement>
|
</TimeInputWithButton>
|
||||||
</Tooltip>
|
|
||||||
</TimeInputWithButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<TimeInputWithButton<TimeActions>
|
||||||
{showLabels && <Editor.Label>Duration</Editor.Label>}
|
name='duration'
|
||||||
<TimeInputWithButton<TimeField>
|
submitHandler={handleSubmit}
|
||||||
name='duration'
|
time={duration}
|
||||||
submitHandler={handleSubmit}
|
disabled={isLockedEnd}
|
||||||
time={duration}
|
placeholder='Duration'
|
||||||
disabled={isLockedEnd}
|
>
|
||||||
placeholder='Duration'
|
<Tooltip label='Lock duration' openDelay={tooltipDelayMid}>
|
||||||
>
|
<InputRightElement
|
||||||
<Tooltip label='Lock duration' openDelay={tooltipDelayMid}>
|
className={activeDuration}
|
||||||
<InputRightElement
|
onClick={() => handleChangeStrategy(TimeStrategy.LockDuration)}
|
||||||
className={activeDuration}
|
data-testid='lock__duration'
|
||||||
onClick={() => handleChangeStrategy(TimeStrategy.LockDuration)}
|
>
|
||||||
data-testid='lock__duration'
|
<span className={style.timeLabel}>D</span>
|
||||||
>
|
{isLockedDuration ? <IoLockClosed /> : <IoLockOpenOutline />}
|
||||||
<span className={style.timeLabel}>D</span>
|
</InputRightElement>
|
||||||
{isLockedDuration ? <IoLockClosed /> : <IoLockOpenOutline />}
|
</Tooltip>
|
||||||
</InputRightElement>
|
</TimeInputWithButton>
|
||||||
</Tooltip>
|
|
||||||
</TimeInputWithButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{warnings.length > 0 && (
|
{warnings.length > 0 && (
|
||||||
<div className={style.timerNote} data-testid='event-warning'>
|
<div className={style.timerNote}>
|
||||||
<Tooltip label={warnings.join(' - ')} openDelay={tooltipDelayFast} variant='ontime-ondark' shouldWrapChildren>
|
<Tooltip label={warnings.join(' - ')} openDelay={tooltipDelayFast} variant='ontime-ondark' shouldWrapChildren>
|
||||||
<IoAlertCircleOutline />
|
<IoAlertCircleOutline />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
|||||||
...timer,
|
...timer,
|
||||||
clock,
|
clock,
|
||||||
timerType: eventNow?.timerType ?? TimerType.CountDown,
|
timerType: eventNow?.timerType ?? TimerType.CountDown,
|
||||||
countToEnd: eventNow?.countToEnd ?? false,
|
isTimeToEnd: eventNow?.isTimeToEnd ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import type { ViewExtendedTimer } from '../../../common/models/TimeManager.type'
|
|||||||
import { timerPlaceholder, timerPlaceholderMin } from '../../../common/utils/styleUtils';
|
import { timerPlaceholder, timerPlaceholderMin } from '../../../common/utils/styleUtils';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
|
||||||
type TimerTypeParams = Pick<ViewExtendedTimer, 'countToEnd' | 'timerType' | 'current' | 'elapsed' | 'clock'>;
|
type TimerTypeParams = Pick<ViewExtendedTimer, 'isTimeToEnd' | 'timerType' | 'current' | 'elapsed' | 'clock'>;
|
||||||
|
|
||||||
export function getTimerByType(freezeEnd: boolean, timerObject?: TimerTypeParams): number | null {
|
export function getTimerByType(freezeEnd: boolean, timerObject?: TimerTypeParams): number | null {
|
||||||
if (!timerObject) {
|
if (!timerObject) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timerObject.countToEnd) {
|
if (timerObject.isTimeToEnd) {
|
||||||
if (timerObject.current === null) {
|
if (timerObject.current === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
|||||||
|
|
||||||
const isPlaying = time.playback !== Playback.Pause;
|
const isPlaying = time.playback !== Playback.Pause;
|
||||||
|
|
||||||
const shouldShowModifiers = time.timerType === TimerType.CountDown || time.countToEnd;
|
const shouldShowModifiers = time.timerType === TimerType.CountDown || time.isTimeToEnd;
|
||||||
const finished = time.phase === TimerPhase.Overtime;
|
const finished = time.phase === TimerPhase.Overtime;
|
||||||
const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage && !hideEndMessage;
|
const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage && !hideEndMessage;
|
||||||
const showFinished =
|
const showFinished =
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export default function Timer(props: TimerProps) {
|
|||||||
const finished = time.phase === TimerPhase.Overtime;
|
const finished = time.phase === TimerPhase.Overtime;
|
||||||
const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0);
|
const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0);
|
||||||
|
|
||||||
const shouldShowModifiers = time.timerType === TimerType.CountDown || time.countToEnd;
|
const shouldShowModifiers = time.timerType === TimerType.CountDown || time.isTimeToEnd;
|
||||||
const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage;
|
const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage;
|
||||||
const showProgress =
|
const showProgress =
|
||||||
eventNow !== null &&
|
eventNow !== null &&
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ $transition-time-feedback: 0.3s;
|
|||||||
|
|
||||||
$component-border-radius-md: 3px;
|
$component-border-radius-md: 3px;
|
||||||
$component-border-radius-sm: 2px;
|
$component-border-radius-sm: 2px;
|
||||||
$component-border-radius-full: 99px;
|
|
||||||
|
|
||||||
// semantic colours
|
// semantic colours
|
||||||
$action-blue: #3182ce;
|
$action-blue: #3182ce;
|
||||||
@@ -51,14 +50,12 @@ $main-spacing: 2rem;
|
|||||||
|
|
||||||
// interface text
|
// interface text
|
||||||
$ontime-font-family: "Open Sans", "Segoe UI", sans-serif;
|
$ontime-font-family: "Open Sans", "Segoe UI", sans-serif;
|
||||||
$title-gray: $gray-200;
|
|
||||||
$label-gray: $gray-400;
|
$label-gray: $gray-400;
|
||||||
$secondary-text-gray: $gray-400;
|
$secondary-text-gray: $gray-400;
|
||||||
$muted-gray: $gray-600;
|
$muted-gray: $gray-600;
|
||||||
$section-white: $ui-white;
|
$section-white: $ui-white;
|
||||||
$inner-section-text-size: calc(1rem - 2px);
|
$inner-section-text-size: calc(1rem - 2px);
|
||||||
$text-body-size: calc(1rem - 1px);
|
$text-body-size: calc(1rem - 1px);
|
||||||
$aux-text-size: calc(1rem - 3px);
|
|
||||||
|
|
||||||
// media queries
|
// media queries
|
||||||
$min-tablet: 500px;
|
$min-tablet: 500px;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const ontimeInputTransparent = {
|
|||||||
...commonStyles,
|
...commonStyles,
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
_hover: {
|
_hover: {
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
backgroundColor: 'rgba(255, 255, 255, 0.10)', // $white-10
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -56,6 +56,6 @@ export const ontimeTextAreaTransparent = {
|
|||||||
...commonStyles,
|
...commonStyles,
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
_hover: {
|
_hover: {
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
backgroundColor: 'rgba(255, 255, 255, 0.10)', // $white-10
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { IconButton, Modal, ModalContent, ModalOverlay, useDisclosure } from '@chakra-ui/react';
|
import { IconButton, Modal, ModalContent, ModalOverlay, useDisclosure } from '@chakra-ui/react';
|
||||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||||
|
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||||
@@ -23,13 +25,14 @@ import styles from './CuesheetPage.module.scss';
|
|||||||
|
|
||||||
export default function CuesheetPage() {
|
export default function CuesheetPage() {
|
||||||
// TODO: can we use the normalised rundown for the table?
|
// TODO: can we use the normalised rundown for the table?
|
||||||
const { data: flatRundown } = useFlatRundown();
|
const { data: flatRundown, status: rundownStatus } = useFlatRundown();
|
||||||
const { data: customFields } = useCustomFields();
|
const { data: customFields } = useCustomFields();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
||||||
const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure();
|
const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure();
|
||||||
const [eventId, setEventId] = useState<string | null>(null);
|
const [eventId, setEventId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { updateCustomField, updateEvent } = useEventAction();
|
||||||
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]);
|
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]);
|
||||||
|
|
||||||
useWindowTitle('Cuesheet');
|
useWindowTitle('Cuesheet');
|
||||||
@@ -40,6 +43,65 @@ export default function CuesheetPage() {
|
|||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
}, [searchParams, setSearchParams]);
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles updating a custom field
|
||||||
|
*/
|
||||||
|
const handleUpdateCustom = useCallback(
|
||||||
|
async (rowIndex: number, accessor: CustomFieldLabel, payload: string) => {
|
||||||
|
if (!flatRundown || rundownStatus !== 'success') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowIndex == null || accessor == null || payload == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if value is the same
|
||||||
|
const event = flatRundown[rowIndex];
|
||||||
|
if (!event || !isOntimeEvent(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip if there is no value change
|
||||||
|
const previousValue = event.custom[accessor];
|
||||||
|
if (previousValue === payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateCustomField(event.id, accessor, payload);
|
||||||
|
},
|
||||||
|
[flatRundown, rundownStatus, updateCustomField],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles updating all other string fields
|
||||||
|
*/
|
||||||
|
const handleUpdate = useCallback(
|
||||||
|
async (rowIndex: number, accessor: keyof OntimeEvent, payload: string) => {
|
||||||
|
if (!flatRundown || rundownStatus !== 'success') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowIndex == null || accessor == null || payload == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if value is the same
|
||||||
|
const event = flatRundown[rowIndex];
|
||||||
|
if (!event || !isOntimeEvent(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip if there is no value change
|
||||||
|
const previousValue = event[accessor];
|
||||||
|
if (previousValue === payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEvent({ id: event.id, [accessor]: payload });
|
||||||
|
},
|
||||||
|
[flatRundown, rundownStatus, updateEvent],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles setting the edit modal target and visibility
|
* Handles setting the edit modal target and visibility
|
||||||
*/
|
*/
|
||||||
@@ -56,7 +118,7 @@ export default function CuesheetPage() {
|
|||||||
[onEventEditorClose, onEventEditorOpen],
|
[onEventEditorClose, onEventEditorOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!customFields || !flatRundown) {
|
if (!customFields || !flatRundown || rundownStatus !== 'success') {
|
||||||
return <EmptyPage text='Loading...' />;
|
return <EmptyPage text='Loading...' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +151,13 @@ export default function CuesheetPage() {
|
|||||||
</CuesheetOverview>
|
</CuesheetOverview>
|
||||||
<CuesheetProgress />
|
<CuesheetProgress />
|
||||||
<CuesheetDnd columns={columns}>
|
<CuesheetDnd columns={columns}>
|
||||||
<CuesheetTable data={flatRundown} columns={columns} showModal={setShowModal} />
|
<CuesheetTable
|
||||||
|
data={flatRundown}
|
||||||
|
columns={columns}
|
||||||
|
handleUpdate={handleUpdate}
|
||||||
|
handleUpdateCustom={handleUpdateCustom}
|
||||||
|
showModal={setShowModal}
|
||||||
|
/>
|
||||||
</CuesheetDnd>
|
</CuesheetDnd>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -32,20 +32,24 @@ $table-header-font-size: calc(1rem - 2px);
|
|||||||
|
|
||||||
.tableHeader,
|
.tableHeader,
|
||||||
.eventRow {
|
.eventRow {
|
||||||
.actionColumn {
|
|
||||||
width: calc(2rem + 0.5rem); // sm button size (--chakra-sizes-8) + 2 * padding
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indexColumn {
|
.indexColumn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
|
||||||
min-width: 3em; // allow for 3-digit numbers
|
min-width: 3em; // allow for 3-digit numbers
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 400;
|
||||||
font-size: $table-header-font-size;
|
font-size: $table-header-font-size;
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
background-color: $gray-1300; // will be overridden inline
|
background-color: $gray-1300; // will be overridden inline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionColumn {
|
||||||
|
width: calc(1.5rem + 0.5rem); // sm button size (--chakra-sizes-6) + 2 * padding
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableHeader {
|
.tableHeader {
|
||||||
@@ -121,6 +125,16 @@ th {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
@include ellipsis-overflow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.delayedTime {
|
.delayedTime {
|
||||||
color: $ontime-delay-text;
|
color: $ontime-delay-text;
|
||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Menu } from '@chakra-ui/react';
|
import { IconButton, Menu, MenuButton } from '@chakra-ui/react';
|
||||||
|
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
||||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
import {
|
import {
|
||||||
|
CustomFieldLabel,
|
||||||
isOntimeBlock,
|
isOntimeBlock,
|
||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
MaybeString,
|
MaybeString,
|
||||||
OntimeEvent,
|
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
OntimeRundownEntry,
|
OntimeRundownEntry,
|
||||||
TimeField,
|
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
|
||||||
import useFollowComponent from '../../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../../common/hooks/useFollowComponent';
|
||||||
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
||||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||||
@@ -32,15 +31,16 @@ import style from './CuesheetTable.module.scss';
|
|||||||
interface CuesheetTableProps {
|
interface CuesheetTableProps {
|
||||||
data: OntimeRundown;
|
data: OntimeRundown;
|
||||||
columns: ColumnDef<OntimeRundownEntry>[];
|
columns: ColumnDef<OntimeRundownEntry>[];
|
||||||
|
handleUpdate: (rowIndex: number, accessor: keyof OntimeRundownEntry, payload: string) => void;
|
||||||
|
handleUpdateCustom: (rowIndex: number, accessor: CustomFieldLabel, payload: string) => void;
|
||||||
showModal: (eventId: MaybeString) => void;
|
showModal: (eventId: MaybeString) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetTable(props: CuesheetTableProps) {
|
export default function CuesheetTable(props: CuesheetTableProps) {
|
||||||
const { data, columns, showModal } = props;
|
const { data, columns, handleUpdate, handleUpdateCustom, showModal } = props;
|
||||||
|
|
||||||
const { updateEvent, updateTimer } = useEventAction();
|
|
||||||
const { selectedEventId } = useSelectedEventId();
|
const { selectedEventId } = useSelectedEventId();
|
||||||
const { followSelected, hideDelays, hidePast, showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
|
const { followSelected, hideDelays, hidePast, hideIndexColumn } = useCuesheetOptions();
|
||||||
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
|
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
|
||||||
useColumnManager(columns);
|
useColumnManager(columns);
|
||||||
|
|
||||||
@@ -57,40 +57,13 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
columnVisibility,
|
columnVisibility,
|
||||||
columnSizing,
|
columnSizing,
|
||||||
},
|
},
|
||||||
|
meta: {
|
||||||
|
handleUpdate,
|
||||||
|
handleUpdateCustom,
|
||||||
|
},
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
onColumnSizingChange: setColumnSizing,
|
onColumnSizingChange: setColumnSizing,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
meta: {
|
|
||||||
handleUpdate: (rowIndex: number, accessor: string, payload: string, isCustom = false) => {
|
|
||||||
// check if value is the same
|
|
||||||
const event = data[rowIndex];
|
|
||||||
if (!event || !isOntimeEvent(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip if there is no value change
|
|
||||||
const key = accessor as keyof OntimeEvent;
|
|
||||||
const previousValue = event[key];
|
|
||||||
if (previousValue === payload) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCustom) {
|
|
||||||
updateEvent({ id: event.id, custom: { [accessor]: payload } });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateEvent({ id: event.id, [accessor]: payload });
|
|
||||||
},
|
|
||||||
handleUpdateTimer: (eventId: string, field: TimeField, payload) => {
|
|
||||||
// the timer element already contains logic to avoid submitting a unchanged value
|
|
||||||
updateTimer(eventId, field, payload, true);
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
showDelayedTimes,
|
|
||||||
hideTableSeconds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const setAllVisible = () => {
|
const setAllVisible = () => {
|
||||||
@@ -119,7 +92,7 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
/>
|
/>
|
||||||
<div ref={tableContainerRef} className={style.cuesheetContainer}>
|
<div ref={tableContainerRef} className={style.cuesheetContainer}>
|
||||||
<table className={style.cuesheet} id='cuesheet'>
|
<table className={style.cuesheet} id='cuesheet'>
|
||||||
<CuesheetHeader headerGroups={headerGroups} />
|
<CuesheetHeader headerGroups={headerGroups} showIndexColumn={!hideIndexColumn} />
|
||||||
<tbody>
|
<tbody>
|
||||||
{rowModel.rows.map((row, index) => {
|
{rowModel.rows.map((row, index) => {
|
||||||
const key = row.original.id;
|
const key = row.original.id;
|
||||||
@@ -172,7 +145,17 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
selectedRef={isSelected ? selectedRef : undefined}
|
selectedRef={isSelected ? selectedRef : undefined}
|
||||||
skip={entry.skip}
|
skip={entry.skip}
|
||||||
colour={entry.colour}
|
colour={entry.colour}
|
||||||
|
showIndexColumn={!hideIndexColumn}
|
||||||
>
|
>
|
||||||
|
<td>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
size='xs'
|
||||||
|
aria-label='Options'
|
||||||
|
icon={<IoEllipsisHorizontal />}
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
{row.getVisibleCells().map((cell) => {
|
{row.getVisibleCells().map((cell) => {
|
||||||
return (
|
return (
|
||||||
<td key={cell.id} style={{ width: cell.column.getSize(), backgroundColor: rowBgColour }}>
|
<td key={cell.id} style={{ width: cell.column.getSize(), backgroundColor: rowBgColour }}>
|
||||||
|
|||||||
+4
-5
@@ -3,7 +3,6 @@ import { flexRender, HeaderGroup } from '@tanstack/react-table';
|
|||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||||
import { useCuesheetOptions } from '../../cuesheet.options';
|
|
||||||
|
|
||||||
import { SortableCell } from './SortableCell';
|
import { SortableCell } from './SortableCell';
|
||||||
|
|
||||||
@@ -11,11 +10,11 @@ import style from '../CuesheetTable.module.scss';
|
|||||||
|
|
||||||
interface CuesheetHeaderProps {
|
interface CuesheetHeaderProps {
|
||||||
headerGroups: HeaderGroup<OntimeRundownEntry>[];
|
headerGroups: HeaderGroup<OntimeRundownEntry>[];
|
||||||
|
showIndexColumn: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetHeader(props: CuesheetHeaderProps) {
|
export default function CuesheetHeader(props: CuesheetHeaderProps) {
|
||||||
const { headerGroups } = props;
|
const { headerGroups, showIndexColumn } = props;
|
||||||
const { hideIndexColumn, showActionMenu } = useCuesheetOptions();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<thead className={style.tableHeader}>
|
<thead className={style.tableHeader}>
|
||||||
@@ -24,8 +23,8 @@ export default function CuesheetHeader(props: CuesheetHeaderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{showActionMenu && <th className={style.actionColumn} />}
|
<th className={style.indexColumn}>{showIndexColumn && '#'}</th>
|
||||||
{!hideIndexColumn && <th className={style.indexColumn}>#</th>}
|
<th className={style.actionColumn} />
|
||||||
<SortableContext key={key} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
<SortableContext key={key} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
const width = header.getSize();
|
const width = header.getSize();
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { memo, MutableRefObject, PropsWithChildren, useLayoutEffect, useRef, useState } from 'react';
|
import { memo, MutableRefObject, PropsWithChildren, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { IconButton, MenuButton } from '@chakra-ui/react';
|
|
||||||
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
|
|
||||||
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||||
import { useCuesheetOptions } from '../../cuesheet.options';
|
|
||||||
|
|
||||||
import style from '../CuesheetTable.module.scss';
|
import style from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
interface EventRowProps {
|
interface EventRowProps {
|
||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
|
showIndexColumn: boolean;
|
||||||
isPast?: boolean;
|
isPast?: boolean;
|
||||||
selectedRef?: MutableRefObject<HTMLTableRowElement | null>;
|
selectedRef?: MutableRefObject<HTMLTableRowElement | null>;
|
||||||
skip?: boolean;
|
skip?: boolean;
|
||||||
@@ -17,8 +15,7 @@ interface EventRowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function EventRow(props: PropsWithChildren<EventRowProps>) {
|
function EventRow(props: PropsWithChildren<EventRowProps>) {
|
||||||
const { children, eventIndex, isPast, selectedRef, skip, colour } = props;
|
const { children, eventIndex, isPast, selectedRef, skip, colour, showIndexColumn } = props;
|
||||||
const { hideIndexColumn, showActionMenu } = useCuesheetOptions();
|
|
||||||
const ownRef = useRef<HTMLTableRowElement>(null);
|
const ownRef = useRef<HTMLTableRowElement>(null);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
@@ -58,22 +55,9 @@ function EventRow(props: PropsWithChildren<EventRowProps>) {
|
|||||||
style={{ opacity: `${isPast ? '0.2' : '1'}` }}
|
style={{ opacity: `${isPast ? '0.2' : '1'}` }}
|
||||||
ref={selectedRef ?? ownRef}
|
ref={selectedRef ?? ownRef}
|
||||||
>
|
>
|
||||||
{showActionMenu && (
|
<td className={style.indexColumn} style={{ backgroundColor, color: mutedText }}>
|
||||||
<td className={style.actionColumn}>
|
{showIndexColumn && eventIndex}
|
||||||
<MenuButton
|
</td>
|
||||||
as={IconButton}
|
|
||||||
size='sm'
|
|
||||||
aria-label='Options'
|
|
||||||
icon={<IoEllipsisHorizontal />}
|
|
||||||
variant='ontime-subtle'
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{!hideIndexColumn && (
|
|
||||||
<td className={style.indexColumn} style={{ backgroundColor, color: mutedText }}>
|
|
||||||
{eventIndex}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{isVisible ? children : null}
|
{isVisible ? children : null}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|||||||
+6
-10
@@ -8,9 +8,7 @@ interface MultiLineCellProps {
|
|||||||
handleUpdate: (newValue: string) => void;
|
handleUpdate: (newValue: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(MultiLineCell);
|
const MultiLineCell = (props: MultiLineCellProps) => {
|
||||||
|
|
||||||
function MultiLineCell(props: MultiLineCellProps) {
|
|
||||||
const { initialValue, handleUpdate } = props;
|
const { initialValue, handleUpdate } = props;
|
||||||
const ref = useRef<HTMLInputElement | null>(null);
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||||
@@ -24,12 +22,8 @@ function MultiLineCell(props: MultiLineCellProps) {
|
|||||||
inputref={ref}
|
inputref={ref}
|
||||||
rows={1}
|
rows={1}
|
||||||
size='sm'
|
size='sm'
|
||||||
style={{
|
padding={0}
|
||||||
minHeight: '2rem',
|
fontSize='1rem'
|
||||||
padding: '0',
|
|
||||||
paddingTop: '0.25rem',
|
|
||||||
fontSize: '1rem',
|
|
||||||
}}
|
|
||||||
transition='none'
|
transition='none'
|
||||||
variant='ontime-transparent'
|
variant='ontime-transparent'
|
||||||
value={value}
|
value={value}
|
||||||
@@ -39,4 +33,6 @@ function MultiLineCell(props: MultiLineCellProps) {
|
|||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default memo(MultiLineCell);
|
||||||
|
|||||||
+5
-26
@@ -1,46 +1,27 @@
|
|||||||
import { forwardRef, memo, useCallback, useImperativeHandle, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import { Input } from '@chakra-ui/react';
|
import { Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
|
|
||||||
interface SingleLineCellProps {
|
interface SingleLineCellProps {
|
||||||
initialValue: string;
|
initialValue: string;
|
||||||
allowSubmitSameValue?: boolean;
|
|
||||||
handleUpdate: (newValue: string) => void;
|
handleUpdate: (newValue: string) => void;
|
||||||
handleCancelUpdate?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
|
const SingleLineCell = (props: SingleLineCellProps) => {
|
||||||
const { initialValue, allowSubmitSameValue, handleUpdate, handleCancelUpdate } = props;
|
const { initialValue, handleUpdate } = props;
|
||||||
const ref = useRef<HTMLInputElement | null>(null);
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||||
|
|
||||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||||
allowSubmitSameValue,
|
|
||||||
submitOnEnter: true, // single line should submit on enter
|
|
||||||
submitOnCtrlEnter: true,
|
submitOnCtrlEnter: true,
|
||||||
onCancelUpdate: handleCancelUpdate,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// expose a subset of the methods to the parent
|
|
||||||
useImperativeHandle(inputRef, () => {
|
|
||||||
return {
|
|
||||||
focus() {
|
|
||||||
ref.current?.focus();
|
|
||||||
},
|
|
||||||
select() {
|
|
||||||
ref.current?.select();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [ref]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
size='sm'
|
size='sx'
|
||||||
variant='ontime-transparent'
|
variant='ontime-transparent'
|
||||||
padding={0}
|
|
||||||
fontSize='md'
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
@@ -49,8 +30,6 @@ const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
|
|||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
SingleLineCell.displayName = 'SingleLineCell';
|
|
||||||
|
|
||||||
export default memo(SingleLineCell);
|
export default memo(SingleLineCell);
|
||||||
|
|||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
/* element attempts matching input styles */
|
|
||||||
.textInput {
|
|
||||||
height: 2rem;
|
|
||||||
background-color: transparent;
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
|
|
||||||
&.delayed {
|
|
||||||
color: $ontime-delay-text;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.muted {
|
|
||||||
// we use opacity instead of colour to handle delayed values
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $gray-1100;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
import { HTMLAttributes, memo, PropsWithChildren } from 'react';
|
|
||||||
|
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
|
||||||
|
|
||||||
import style from './TextLikeInput.module.scss';
|
|
||||||
|
|
||||||
export default memo(TextLikeInput);
|
|
||||||
|
|
||||||
interface TextLikeInputProps extends HTMLAttributes<HTMLSpanElement> {
|
|
||||||
delayed?: boolean;
|
|
||||||
muted?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TextLikeInput(props: PropsWithChildren<TextLikeInputProps>) {
|
|
||||||
const { delayed, muted, children, className, ...elementProps } = props;
|
|
||||||
const classes = cx([style.textInput, delayed && style.delayed, muted && style.muted, className]);
|
|
||||||
return (
|
|
||||||
<div className={classes} {...elementProps} tabIndex={0}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { memo, PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
|
||||||
|
|
||||||
import SingleLineCell from './SingleLineCell';
|
|
||||||
import TextLikeInput from './TextLikeInput';
|
|
||||||
|
|
||||||
interface TimeInputDurationProps {
|
|
||||||
initialValue: number;
|
|
||||||
lockedValue: boolean;
|
|
||||||
delayed?: boolean;
|
|
||||||
onSubmit: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(TimeInputDuration);
|
|
||||||
|
|
||||||
function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
|
|
||||||
const { initialValue, lockedValue, delayed, onSubmit, children } = props;
|
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [value, setValue] = useState(initialValue);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
// when we go into edit mode, set focus to the input
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEditing && inputRef.current) {
|
|
||||||
inputRef.current.focus();
|
|
||||||
inputRef.current.select();
|
|
||||||
}
|
|
||||||
}, [isEditing]);
|
|
||||||
|
|
||||||
// reset value when initialValue changes, avoiding interrupting the user if we are in edit mode
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isEditing) {
|
|
||||||
setValue(initialValue);
|
|
||||||
}
|
|
||||||
}, [initialValue, isEditing]);
|
|
||||||
|
|
||||||
const handleFakeFocus = () => setIsEditing(true);
|
|
||||||
const handleFakeBlur = () => setIsEditing(false);
|
|
||||||
|
|
||||||
const handleUpdate = useCallback(
|
|
||||||
(newValue: string) => {
|
|
||||||
setIsEditing(false);
|
|
||||||
|
|
||||||
// if the user sends an empty string, we want to clear the value
|
|
||||||
if (newValue === '') {
|
|
||||||
onSubmit(newValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we dont know the values in the rundown, escalate to handler
|
|
||||||
if (newValue.startsWith('p') || newValue.startsWith('+')) {
|
|
||||||
onSubmit(newValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const valueInMillis = parseUserTime(newValue);
|
|
||||||
if (valueInMillis < 0 || isNaN(valueInMillis)) {
|
|
||||||
setValue(initialValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the value is the same, we may still want to push the lock change
|
|
||||||
if (valueInMillis === initialValue && lockedValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit(newValue);
|
|
||||||
setValue(Number(newValue));
|
|
||||||
},
|
|
||||||
[initialValue, lockedValue, onSubmit],
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeString = millisToString(value);
|
|
||||||
|
|
||||||
return isEditing ? (
|
|
||||||
<SingleLineCell
|
|
||||||
ref={inputRef}
|
|
||||||
initialValue={timeString}
|
|
||||||
allowSubmitSameValue={!lockedValue} // if the value is not locked, submitting will lock the value
|
|
||||||
handleUpdate={handleUpdate}
|
|
||||||
handleCancelUpdate={handleFakeBlur}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TextLikeInput onClick={handleFakeFocus} onFocus={handleFakeFocus} muted={!lockedValue} delayed={delayed}>
|
|
||||||
{children}
|
|
||||||
</TextLikeInput>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+27
-83
@@ -1,95 +1,44 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
import { CustomFields, isOntimeEvent, OntimeEvent, OntimeRundownEntry, TimeStrategy } from 'ontime-types';
|
import { CustomFields, isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
||||||
import { millisToString, removeSeconds } from 'ontime-utils';
|
|
||||||
|
|
||||||
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
|
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
|
||||||
import { formatDuration } from '../../../../common/utils/time';
|
import RunningTime from '../../../../features/viewers/common/running-time/RunningTime';
|
||||||
|
import { useCuesheetOptions } from '../../cuesheet.options';
|
||||||
|
|
||||||
import MultiLineCell from './MultiLineCell';
|
import MultiLineCell from './MultiLineCell';
|
||||||
import SingleLineCell from './SingleLineCell';
|
import SingleLineCell from './SingleLineCell';
|
||||||
import TimeInput from './TimeInput';
|
|
||||||
|
|
||||||
function MakeStart({ getValue, row, table }: CellContext<OntimeRundownEntry, unknown>) {
|
import style from '../CuesheetTable.module.scss';
|
||||||
if (!table.options.meta) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { handleUpdateTimer } = table.options.meta;
|
function MakeTimer({ getValue, row: { original } }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
const { showDelayedTimes, hideTableSeconds } = table.options.meta.options;
|
const { showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
|
||||||
|
const cellValue = (getValue() as number | null) ?? 0;
|
||||||
const update = (newValue: string) => handleUpdateTimer(row.original.id, 'timeStart', newValue);
|
const delayValue = (original as OntimeEvent)?.delay ?? 0;
|
||||||
|
|
||||||
const startTime = getValue() as number;
|
|
||||||
const isStartLocked = (row.original as OntimeEvent).linkStart === null;
|
|
||||||
const delayValue = (row.original as OntimeEvent)?.delay ?? 0;
|
|
||||||
|
|
||||||
const displayTime = showDelayedTimes ? startTime + delayValue : startTime;
|
|
||||||
let formattedTime = millisToString(displayTime);
|
|
||||||
if (hideTableSeconds) {
|
|
||||||
formattedTime = removeSeconds(formattedTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimeInput initialValue={startTime} onSubmit={update} lockedValue={isStartLocked} delayed={delayValue !== 0}>
|
<span className={style.time}>
|
||||||
{formattedTime}
|
<DelayIndicator delayValue={delayValue} />
|
||||||
<DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(startTime)} />
|
<RunningTime value={cellValue} hideSeconds={hideTableSeconds} />
|
||||||
</TimeInput>
|
{delayValue !== 0 && showDelayedTimes && (
|
||||||
|
<RunningTime className={style.delayedTime} value={cellValue + delayValue} hideSeconds={hideTableSeconds} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MakeEnd({ getValue, row, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeDuration({ getValue }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
if (!table.options.meta) {
|
const { hideTableSeconds } = useCuesheetOptions();
|
||||||
return null;
|
const cellValue = (getValue() as number | null) ?? 0;
|
||||||
}
|
|
||||||
|
|
||||||
const { handleUpdateTimer } = table.options.meta;
|
return <RunningTime value={cellValue} hideSeconds={hideTableSeconds} />;
|
||||||
const { showDelayedTimes, hideTableSeconds } = table.options.meta.options;
|
|
||||||
|
|
||||||
const update = (newValue: string) => handleUpdateTimer(row.original.id, 'timeEnd', newValue);
|
|
||||||
|
|
||||||
const endTime = getValue() as number;
|
|
||||||
const isEndLocked = (row.original as OntimeEvent).timeStrategy === TimeStrategy.LockEnd;
|
|
||||||
const delayValue = (row.original as OntimeEvent)?.delay ?? 0;
|
|
||||||
|
|
||||||
const displayTime = showDelayedTimes ? endTime + delayValue : endTime;
|
|
||||||
let formattedTime = millisToString(displayTime);
|
|
||||||
if (hideTableSeconds) {
|
|
||||||
formattedTime = removeSeconds(formattedTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TimeInput initialValue={endTime} onSubmit={update} lockedValue={isEndLocked} delayed={delayValue !== 0}>
|
|
||||||
{formattedTime}
|
|
||||||
<DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(endTime)} />
|
|
||||||
</TimeInput>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MakeDuration({ getValue, row, table }: CellContext<OntimeRundownEntry, unknown>) {
|
|
||||||
if (!table.options.meta) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { handleUpdateTimer } = table.options.meta;
|
|
||||||
|
|
||||||
const update = (newValue: string) => handleUpdateTimer(row.original.id, 'duration', newValue);
|
|
||||||
|
|
||||||
const duration = getValue() as number;
|
|
||||||
const isDurationLocked = (row.original as OntimeEvent).timeStrategy === TimeStrategy.LockDuration;
|
|
||||||
const formattedDuration = formatDuration(duration, false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TimeInput initialValue={duration} onSubmit={update} lockedValue={isDurationLocked}>
|
|
||||||
{formattedDuration}
|
|
||||||
</TimeInput>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
// @ts-expect-error -- we inject this into react-table
|
||||||
|
table.options.meta?.handleUpdate(row.index, column.id, newValue);
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
||||||
[column.id, row.index],
|
[column.id, row.index],
|
||||||
@@ -108,7 +57,8 @@ function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEnt
|
|||||||
function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
// @ts-expect-error -- we inject this into react-table
|
||||||
|
table.options.meta?.handleUpdate(row.index, column.id, newValue);
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
||||||
[column.id, row.index],
|
[column.id, row.index],
|
||||||
@@ -127,7 +77,8 @@ function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEn
|
|||||||
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
// @ts-expect-error -- we inject this into react-table
|
||||||
|
table.options.meta?.handleUpdateCustom(row.index, column.id, newValue);
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
||||||
[column.id, row.index],
|
[column.id, row.index],
|
||||||
@@ -151,7 +102,6 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
|||||||
meta: { colour: customFields[key].colour },
|
meta: { colour: customFields[key].colour },
|
||||||
cell: MakeCustomField,
|
cell: MakeCustomField,
|
||||||
size: 250,
|
size: 250,
|
||||||
minSize: 75,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -161,23 +111,20 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
|||||||
header: 'Cue',
|
header: 'Cue',
|
||||||
cell: MakeSingleLineField,
|
cell: MakeSingleLineField,
|
||||||
size: 75,
|
size: 75,
|
||||||
minSize: 40,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'timeStart',
|
accessorKey: 'timeStart',
|
||||||
id: 'timeStart',
|
id: 'timeStart',
|
||||||
header: 'Start',
|
header: 'Start',
|
||||||
cell: MakeStart,
|
cell: MakeTimer,
|
||||||
size: 75,
|
size: 75,
|
||||||
minSize: 75,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'timeEnd',
|
accessorKey: 'timeEnd',
|
||||||
id: 'timeEnd',
|
id: 'timeEnd',
|
||||||
header: 'End',
|
header: 'End',
|
||||||
cell: MakeEnd,
|
cell: MakeTimer,
|
||||||
size: 75,
|
size: 75,
|
||||||
minSize: 75,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'duration',
|
accessorKey: 'duration',
|
||||||
@@ -185,7 +132,6 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
|||||||
header: 'Duration',
|
header: 'Duration',
|
||||||
cell: MakeDuration,
|
cell: MakeDuration,
|
||||||
size: 75,
|
size: 75,
|
||||||
minSize: 75,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'title',
|
accessorKey: 'title',
|
||||||
@@ -193,7 +139,6 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
|||||||
header: 'Title',
|
header: 'Title',
|
||||||
cell: MakeSingleLineField,
|
cell: MakeSingleLineField,
|
||||||
size: 250,
|
size: 250,
|
||||||
minSize: 75,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'note',
|
accessorKey: 'note',
|
||||||
@@ -201,7 +146,6 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
|||||||
header: 'Note',
|
header: 'Note',
|
||||||
cell: MakeMultiLineField,
|
cell: MakeMultiLineField,
|
||||||
size: 250,
|
size: 250,
|
||||||
minSize: 75,
|
|
||||||
},
|
},
|
||||||
...dynamicCustomFields,
|
...dynamicCustomFields,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,13 +10,6 @@ import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
|||||||
*/
|
*/
|
||||||
export const cuesheetOptions: ViewOption[] = [
|
export const cuesheetOptions: ViewOption[] = [
|
||||||
{ section: 'Table options' },
|
{ section: 'Table options' },
|
||||||
{
|
|
||||||
id: 'showActionMenu',
|
|
||||||
title: 'Show action menu',
|
|
||||||
description: 'Whether to show the action menu for every row in the table',
|
|
||||||
type: 'boolean',
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'hideTableSeconds',
|
id: 'hideTableSeconds',
|
||||||
title: 'Hide seconds in table',
|
title: 'Hide seconds in table',
|
||||||
@@ -63,7 +56,6 @@ export const cuesheetOptions: ViewOption[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
type CuesheetOptions = {
|
type CuesheetOptions = {
|
||||||
showActionMenu: boolean;
|
|
||||||
hideTableSeconds: boolean;
|
hideTableSeconds: boolean;
|
||||||
followSelected: boolean;
|
followSelected: boolean;
|
||||||
hidePast: boolean;
|
hidePast: boolean;
|
||||||
@@ -79,7 +71,6 @@ type CuesheetOptions = {
|
|||||||
export function getOptionsFromParams(searchParams: URLSearchParams): CuesheetOptions {
|
export function getOptionsFromParams(searchParams: URLSearchParams): CuesheetOptions {
|
||||||
// we manually make an object that matches the key above
|
// we manually make an object that matches the key above
|
||||||
return {
|
return {
|
||||||
showActionMenu: isStringBoolean(searchParams.get('showActionMenu')),
|
|
||||||
hideTableSeconds: isStringBoolean(searchParams.get('hideTableSeconds')),
|
hideTableSeconds: isStringBoolean(searchParams.get('hideTableSeconds')),
|
||||||
followSelected: isStringBoolean(searchParams.get('followSelected')),
|
followSelected: isStringBoolean(searchParams.get('followSelected')),
|
||||||
hidePast: isStringBoolean(searchParams.get('hidePast')),
|
hidePast: isStringBoolean(searchParams.get('hidePast')),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useViewportSize } from '@mantine/hooks';
|
import { useViewportSize } from '@mantine/hooks';
|
||||||
import { isOntimeEvent, isPlayableEvent, OntimeRundown } from 'ontime-types';
|
import { isOntimeEvent, isPlayableEvent, MaybeNumber, OntimeRundown } from 'ontime-types';
|
||||||
import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
import { checkIsNextDay, dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
|
|
||||||
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
||||||
import { getElementPosition, getEndHour, getStartHour } from './timeline.utils';
|
import { getElementPosition, getEndHour, getStartHour } from './timeline.utils';
|
||||||
@@ -30,8 +30,10 @@ function Timeline(props: TimelineProps) {
|
|||||||
const startHour = getStartHour(firstStart);
|
const startHour = getStartHour(firstStart);
|
||||||
const endHour = getEndHour(firstStart + totalDuration + (lastEvent?.delay ?? 0));
|
const endHour = getEndHour(firstStart + totalDuration + (lastEvent?.delay ?? 0));
|
||||||
|
|
||||||
|
let previousEventStartTime: MaybeNumber = null;
|
||||||
// we use selectedEventId as a signifier on whether the timeline is live
|
// we use selectedEventId as a signifier on whether the timeline is live
|
||||||
let eventStatus: ProgressStatus = selectedEventId ? 'done' : 'future';
|
let eventStatus: ProgressStatus = selectedEventId ? 'done' : 'future';
|
||||||
|
let elapsedDays = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.timeline}>
|
<div className={style.timeline}>
|
||||||
@@ -50,7 +52,14 @@ function Timeline(props: TimelineProps) {
|
|||||||
eventStatus = 'live';
|
eventStatus = 'live';
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalisedStart = event.timeStart + event.dayOffset * dayInMs;
|
// we only need to check for next day if we have a previous event
|
||||||
|
if (
|
||||||
|
previousEventStartTime !== null &&
|
||||||
|
checkIsNextDay(previousEventStartTime, event.timeStart, event.duration)
|
||||||
|
) {
|
||||||
|
elapsedDays++;
|
||||||
|
}
|
||||||
|
const normalisedStart = event.timeStart + elapsedDays * dayInMs;
|
||||||
|
|
||||||
const { left: elementLeftPosition, width: elementWidth } = getElementPosition(
|
const { left: elementLeftPosition, width: elementWidth } = getElementPosition(
|
||||||
startHour * MILLIS_PER_HOUR,
|
startHour * MILLIS_PER_HOUR,
|
||||||
@@ -60,6 +69,9 @@ function Timeline(props: TimelineProps) {
|
|||||||
screenWidth,
|
screenWidth,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// prepare values for next iteration
|
||||||
|
previousEventStartTime = normalisedStart;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimelineEntry
|
<TimelineEntry
|
||||||
key={event.id}
|
key={event.id}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export function useScopedRundown(rundown: OntimeRundown, selectedEventId: MaybeS
|
|||||||
let selectedIndex = selectedEventId ? Infinity : -1;
|
let selectedIndex = selectedEventId ? Infinity : -1;
|
||||||
let firstStart = null;
|
let firstStart = null;
|
||||||
let totalDuration = 0;
|
let totalDuration = 0;
|
||||||
let lastEntry: PlayableEvent | undefined;
|
let lastEntry: PlayableEvent | null = null;
|
||||||
|
|
||||||
for (let i = 0; i < rundown.length; i++) {
|
for (let i = 0; i < rundown.length; i++) {
|
||||||
const currentEntry = rundown[i];
|
const currentEntry = rundown[i];
|
||||||
@@ -142,7 +142,12 @@ export function useScopedRundown(rundown: OntimeRundown, selectedEventId: MaybeS
|
|||||||
firstStart = currentEntry.timeStart;
|
firstStart = currentEntry.timeStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeFromPrevious: number = getTimeFromPrevious(currentEntry, lastEntry);
|
const timeFromPrevious: number = getTimeFromPrevious(
|
||||||
|
currentEntry.timeStart,
|
||||||
|
lastEntry?.timeStart,
|
||||||
|
lastEntry?.timeEnd,
|
||||||
|
lastEntry?.duration,
|
||||||
|
);
|
||||||
|
|
||||||
if (timeFromPrevious === 0) {
|
if (timeFromPrevious === 0) {
|
||||||
totalDuration += currentEntry.duration;
|
totalDuration += currentEntry.duration;
|
||||||
@@ -151,7 +156,7 @@ export function useScopedRundown(rundown: OntimeRundown, selectedEventId: MaybeS
|
|||||||
} else if (timeFromPrevious < 0) {
|
} else if (timeFromPrevious < 0) {
|
||||||
totalDuration += Math.max(currentEntry.duration + timeFromPrevious, 0);
|
totalDuration += Math.max(currentEntry.duration + timeFromPrevious, 0);
|
||||||
}
|
}
|
||||||
if (isNewLatest(currentEntry, lastEntry)) {
|
if (isNewLatest(currentEntry.timeStart, currentEntry.timeEnd, lastEntry?.timeStart, lastEntry?.timeEnd)) {
|
||||||
lastEntry = currentEntry;
|
lastEntry = currentEntry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-electron",
|
"name": "ontime",
|
||||||
"version": "3.10.3",
|
"version": "3.9.5",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -109,13 +109,8 @@ function askToQuit() {
|
|||||||
* Allows processes to escalate errors to be shown in electron
|
* Allows processes to escalate errors to be shown in electron
|
||||||
* @param {string} error
|
* @param {string} error
|
||||||
*/
|
*/
|
||||||
function escalateError(error, unrecoverable = false) {
|
function escalateError(error) {
|
||||||
if (unrecoverable) {
|
dialog.showErrorBox('An unrecoverable error occurred', error);
|
||||||
dialog.showErrorBox('An unrecoverable error occurred', error);
|
|
||||||
appShutdown();
|
|
||||||
} else {
|
|
||||||
dialog.showErrorBox('An error occurred', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+10
-10
@@ -2,25 +2,25 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "3.10.3",
|
"version": "3.9.5",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/sheets": "^5.0.5",
|
"@googleapis/sheets": "^5.0.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"express": "^4.21.1",
|
"express": "^4.18.2",
|
||||||
"express-static-gzip": "^2.2.0",
|
"express-static-gzip": "^2.1.7",
|
||||||
"express-validator": "^7.2.0",
|
"express-validator": "^7.0.1",
|
||||||
"fast-equals": "^5.0.1",
|
"fast-equals": "^5.0.1",
|
||||||
"google-auth-library": "^9.4.2",
|
"google-auth-library": "^9.4.2",
|
||||||
"got": "^14.4.5",
|
"got": "^14.0.0",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-osc": "^9.1.4",
|
"node-osc": "^9.0.2",
|
||||||
"ontime-utils": "workspace:*",
|
"ontime-utils": "workspace:*",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"steno": "^4.0.2",
|
"steno": "^4.0.2",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.13.0",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/multer": "^1.4.11",
|
"@types/multer": "^1.4.11",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@types/node-osc": "^6.0.3",
|
"@types/node-osc": "^6.0.2",
|
||||||
"@types/websocket": "^1.0.5",
|
"@types/websocket": "^1.0.5",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
"prettier": "catalog:",
|
"prettier": "catalog:",
|
||||||
"server-timing": "^3.3.3",
|
"server-timing": "^3.3.3",
|
||||||
"shx": "^0.3.4",
|
"shx": "^0.3.4",
|
||||||
"ts-essentials": "^10.0.3",
|
"ts-essentials": "^9.4.1",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.16.2",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vitest": "catalog:"
|
"vitest": "catalog:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function generateRundownPreview(options: ImportMap): { rundown: OntimeRun
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clear the data
|
// clear the data
|
||||||
excelData = xlsx.utils.book_new();
|
excelData = undefined;
|
||||||
|
|
||||||
return { rundown, customFields };
|
return { rundown, customFields };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { GetInfo, SessionStats } from 'ontime-types';
|
|||||||
|
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { publicDir } from '../../setup/index.js';
|
import { publicDir } from '../../setup/index.js';
|
||||||
|
import { getNetworkInterfaces } from '../../utils/networkInterfaces.js';
|
||||||
import { socket } from '../../adapters/WebsocketAdapter.js';
|
import { socket } from '../../adapters/WebsocketAdapter.js';
|
||||||
import { getLastRequest } from '../../api-integration/integration.controller.js';
|
import { getLastRequest } from '../../api-integration/integration.controller.js';
|
||||||
import { getLastLoadedProject } from '../../services/app-state-service/AppStateService.js';
|
import { getLastLoadedProject } from '../../services/app-state-service/AppStateService.js';
|
||||||
import { runtimeService } from '../../services/runtime-service/RuntimeService.js';
|
import { runtimeService } from '../../services/runtime-service/RuntimeService.js';
|
||||||
import { getNetworkInterfaces } from '../../utils/network.js';
|
|
||||||
|
|
||||||
const startedAt = new Date();
|
const startedAt = new Date();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const validateRequestConnection = [
|
|||||||
.exists()
|
.exists()
|
||||||
.isString()
|
.isString()
|
||||||
.isLength({
|
.isLength({
|
||||||
min: 20,
|
min: 40,
|
||||||
max: 100,
|
max: 100,
|
||||||
})
|
})
|
||||||
.withMessage('Sheet ID is usually 44 characters long'),
|
.withMessage('Sheet ID is usually 44 characters long'),
|
||||||
|
|||||||
+73
-16
@@ -1,14 +1,12 @@
|
|||||||
import { LogOrigin, Playback, runtimeStorePlaceholder, SimpleDirection, SimplePlayback } from 'ontime-types';
|
import { LogOrigin, Playback, SimpleDirection, SimplePlayback } from 'ontime-types';
|
||||||
|
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import expressStaticGzip from 'express-static-gzip';
|
||||||
import http, { Server } from 'http';
|
import http, { Server } from 'http';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import serverTiming from 'server-timing';
|
import serverTiming from 'server-timing';
|
||||||
|
import { extname } from 'node:path';
|
||||||
// Import middleware configuration
|
|
||||||
import { bodyParser } from './middleware/bodyParser.js';
|
|
||||||
import { compressedStatic } from './middleware/staticGZip.js';
|
|
||||||
|
|
||||||
// import utils
|
// import utils
|
||||||
import { publicDir, srcDir, srcFiles } from './setup/index.js';
|
import { publicDir, srcDir, srcFiles } from './setup/index.js';
|
||||||
@@ -42,8 +40,8 @@ import { initialiseProject } from './services/project-service/ProjectService.js'
|
|||||||
// Utilities
|
// Utilities
|
||||||
import { clearUploadfolder } from './utils/upload.js';
|
import { clearUploadfolder } from './utils/upload.js';
|
||||||
import { generateCrashReport } from './utils/generateCrashReport.js';
|
import { generateCrashReport } from './utils/generateCrashReport.js';
|
||||||
|
import { getNetworkInterfaces } from './utils/networkInterfaces.js';
|
||||||
import { timerConfig } from './config/config.js';
|
import { timerConfig } from './config/config.js';
|
||||||
import { serverTryDesiredPort, getNetworkInterfaces } from './utils/network.js';
|
|
||||||
|
|
||||||
console.log('\n');
|
console.log('\n');
|
||||||
consoleHighlight(`Starting Ontime version ${ONTIME_VERSION}`);
|
consoleHighlight(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||||
@@ -79,8 +77,8 @@ app.use(cors());
|
|||||||
app.options('*', cors());
|
app.options('*', cors());
|
||||||
|
|
||||||
// Implement middleware
|
// Implement middleware
|
||||||
app.use(bodyParser);
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(prefix, compressedStatic);
|
app.use(express.json({ limit: '1mb' }));
|
||||||
|
|
||||||
// Implement route endpoints
|
// Implement route endpoints
|
||||||
app.use(`${prefix}/data`, appRouter); // router for application data
|
app.use(`${prefix}/data`, appRouter); // router for application data
|
||||||
@@ -94,6 +92,31 @@ app.use(`${prefix}/external`, (req, res) => {
|
|||||||
});
|
});
|
||||||
app.use(`${prefix}/user`, express.static(publicDir.userDir));
|
app.use(`${prefix}/user`, express.static(publicDir.userDir));
|
||||||
|
|
||||||
|
// serve static - react, in dev/test mode we fetch the React app from module
|
||||||
|
app.use(
|
||||||
|
prefix,
|
||||||
|
expressStaticGzip(srcDir.clientDir, {
|
||||||
|
enableBrotli: true,
|
||||||
|
orderPreference: ['br'],
|
||||||
|
// when we build the client the file names contain a unique hash for the build
|
||||||
|
// this allows us to use the immutable tag
|
||||||
|
// as the contents of a build file will never change without also changing its name
|
||||||
|
// meaning that the client does not need to revalidate the contents with the server
|
||||||
|
serveStatic: {
|
||||||
|
etag: false,
|
||||||
|
lastModified: false,
|
||||||
|
immutable: true,
|
||||||
|
maxAge: '1y',
|
||||||
|
setHeaders: (res, file) => {
|
||||||
|
// make sure the HTML files are always revalidated
|
||||||
|
if (extname(file) === '.html') {
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=0');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
app.get(`${prefix}/*`, (_req, res) => {
|
app.get(`${prefix}/*`, (_req, res) => {
|
||||||
res.sendFile(srcFiles.clientIndexHtml);
|
res.sendFile(srcFiles.clientIndexHtml);
|
||||||
});
|
});
|
||||||
@@ -153,17 +176,15 @@ export const initAssets = async () => {
|
|||||||
* Starts servers
|
* Starts servers
|
||||||
*/
|
*/
|
||||||
export const startServer = async (
|
export const startServer = async (
|
||||||
escalateErrorFn?: (error: string, unrecoverable: boolean) => void,
|
escalateErrorFn?: (error: string) => void,
|
||||||
): Promise<{ message: string; serverPort: number }> => {
|
): Promise<{ message: string; serverPort: number }> => {
|
||||||
checkStart(OntimeStartOrder.InitServer);
|
checkStart(OntimeStartOrder.InitServer);
|
||||||
// initialise logging service, escalateErrorFn only exists in electron
|
|
||||||
logger.init(escalateErrorFn);
|
|
||||||
const settings = getDataProvider().getSettings();
|
const settings = getDataProvider().getSettings();
|
||||||
const { serverPort: desiredPort } = settings;
|
const { serverPort: desiredPort } = settings;
|
||||||
|
|
||||||
expressServer = http.createServer(app);
|
expressServer = http.createServer(app);
|
||||||
|
|
||||||
// the express server must be started before the socket otherwise the on error event listener will not attach properly
|
// the express server must be started before the socket otherwise the on error eventlissner will not attach properly
|
||||||
const resultPort = await serverTryDesiredPort(expressServer, desiredPort);
|
const resultPort = await serverTryDesiredPort(expressServer, desiredPort);
|
||||||
await getDataProvider().setSettings({ ...settings, serverPort: resultPort });
|
await getDataProvider().setSettings({ ...settings, serverPort: resultPort });
|
||||||
|
|
||||||
@@ -177,7 +198,7 @@ export const startServer = async (
|
|||||||
clock: state.clock,
|
clock: state.clock,
|
||||||
timer: state.timer,
|
timer: state.timer,
|
||||||
onAir: state.timer.playback !== Playback.Stop,
|
onAir: state.timer.playback !== Playback.Stop,
|
||||||
message: { ...runtimeStorePlaceholder.message },
|
message: messageService.getState(),
|
||||||
runtime: state.runtime,
|
runtime: state.runtime,
|
||||||
eventNow: state.eventNow,
|
eventNow: state.eventNow,
|
||||||
currentBlock: {
|
currentBlock: {
|
||||||
@@ -196,14 +217,14 @@ export const startServer = async (
|
|||||||
ping: -1,
|
ping: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// initialise logging service, escalateErrorFn is only exists in electron
|
||||||
|
logger.init(escalateErrorFn);
|
||||||
|
|
||||||
// initialise rundown service
|
// initialise rundown service
|
||||||
const persistedRundown = getDataProvider().getRundown();
|
const persistedRundown = getDataProvider().getRundown();
|
||||||
const persistedCustomFields = getDataProvider().getCustomFields();
|
const persistedCustomFields = getDataProvider().getCustomFields();
|
||||||
initRundown(persistedRundown, persistedCustomFields);
|
initRundown(persistedRundown, persistedCustomFields);
|
||||||
|
|
||||||
// initialise message service
|
|
||||||
messageService.init(eventStore.set, eventStore.get);
|
|
||||||
|
|
||||||
// load restore point if it exists
|
// load restore point if it exists
|
||||||
const maybeRestorePoint = await restoreService.load();
|
const maybeRestorePoint = await restoreService.load();
|
||||||
|
|
||||||
@@ -310,3 +331,39 @@ process.on('uncaughtException', async (error) => {
|
|||||||
process.once('SIGHUP', async () => shutdown(0));
|
process.once('SIGHUP', async () => shutdown(0));
|
||||||
process.once('SIGINT', async () => shutdown(0));
|
process.once('SIGINT', async () => shutdown(0));
|
||||||
process.once('SIGTERM', async () => shutdown(0));
|
process.once('SIGTERM', async () => shutdown(0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description tries to open the server with the desired port, and if getting a `EADDRINUSE` will change to an efemeral port
|
||||||
|
* @param {http.Server}server http server object
|
||||||
|
* @param {number}desiredPort the desired port
|
||||||
|
* @returns {number} the resulting port number
|
||||||
|
* @throws any other server errors will result in a throw
|
||||||
|
*/
|
||||||
|
async function serverTryDesiredPort(server: http.Server, desiredPort: number): Promise<number> {
|
||||||
|
return new Promise((res) => {
|
||||||
|
expressServer.once('error', (e) => {
|
||||||
|
if (testForPortInUser(e)) {
|
||||||
|
logger.crash(LogOrigin.Server, `Failed open the desired port: ${desiredPort} | to moving to Ephemeral port`);
|
||||||
|
server.listen(0, '0.0.0.0', () => {
|
||||||
|
// @ts-expect-error TODO: find proper documentation for this api
|
||||||
|
const port: number = server.address().port;
|
||||||
|
res(port);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
server.listen(desiredPort, '0.0.0.0', () => {
|
||||||
|
// @ts-expect-error TODO: find proper documentation for this api
|
||||||
|
const port: number = server.address().port;
|
||||||
|
res(port);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testForPortInUser(err: unknown) {
|
||||||
|
if (typeof err === 'object' && 'code' in err && err.code === 'EADDRINUSE') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { isProduction } from '../externals.js';
|
|||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
private queue: Log[];
|
private queue: Log[];
|
||||||
private escalateErrorFn: ((error: string, unrecoverable: boolean) => void) | null;
|
private escalateErrorFn: ((error: string) => void) | null;
|
||||||
private canLog = false;
|
private canLog = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -20,7 +20,7 @@ class Logger {
|
|||||||
/**
|
/**
|
||||||
* Enabling setup logger after init
|
* Enabling setup logger after init
|
||||||
*/
|
*/
|
||||||
init(escalateErrorFn?: (error: string, unrecoverable: boolean) => void) {
|
init(escalateErrorFn?: (error: string) => void) {
|
||||||
// flush logs from queue
|
// flush logs from queue
|
||||||
this.queue.forEach((log) => {
|
this.queue.forEach((log) => {
|
||||||
this._push(log);
|
this._push(log);
|
||||||
@@ -103,11 +103,8 @@ class Logger {
|
|||||||
* @param origin
|
* @param origin
|
||||||
* @param text
|
* @param text
|
||||||
*/
|
*/
|
||||||
error(origin: string, text: string, escalate = false) {
|
error(origin: string, text: string) {
|
||||||
this.emit(LogLevel.Error, origin, text);
|
this.emit(LogLevel.Error, origin, text);
|
||||||
if (escalate) {
|
|
||||||
this.escalateErrorFn?.(text, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,7 +114,7 @@ class Logger {
|
|||||||
*/
|
*/
|
||||||
crash(origin: string, text: string) {
|
crash(origin: string, text: string) {
|
||||||
this.emit(LogLevel.Severe, origin, text);
|
this.emit(LogLevel.Severe, origin, text);
|
||||||
this.escalateErrorFn?.(text, true);
|
this.escalateErrorFn?.(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function getCustomFields(): Readonly<CustomFields> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setRundown(newData: OntimeRundown): ReadonlyPromise<OntimeRundown> {
|
async function setRundown(newData: OntimeRundown): ReadonlyPromise<OntimeRundown> {
|
||||||
db.data.rundown = newData;
|
db.data.rundown = [...newData];
|
||||||
await persist();
|
await persist();
|
||||||
return db.data.rundown;
|
return db.data.rundown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ describe('safeMerge', () => {
|
|||||||
publicInfo: '',
|
publicInfo: '',
|
||||||
backstageUrl: '',
|
backstageUrl: '',
|
||||||
backstageInfo: '',
|
backstageInfo: '',
|
||||||
projectLogo: null,
|
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
|
|
||||||
export const bodyParser = [express.urlencoded({ extended: true }), express.json({ limit: '1mb' })];
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import expressStaticGzip from 'express-static-gzip';
|
|
||||||
import { extname } from 'node:path';
|
|
||||||
|
|
||||||
import { srcDir } from '../setup/index.js';
|
|
||||||
|
|
||||||
// serve static - react, in dev/test mode we fetch the React app from module
|
|
||||||
export const compressedStatic = expressStaticGzip(srcDir.clientDir, {
|
|
||||||
enableBrotli: true,
|
|
||||||
orderPreference: ['br'],
|
|
||||||
// when we build the client the file names contain a unique hash for the build
|
|
||||||
// this allows us to use the immutable tag
|
|
||||||
// as the contents of a build file will never change without also changing its name
|
|
||||||
// meaning that the client does not need to revalidate the contents with the server
|
|
||||||
serveStatic: {
|
|
||||||
etag: false,
|
|
||||||
lastModified: false,
|
|
||||||
immutable: true,
|
|
||||||
maxAge: '1y',
|
|
||||||
setHeaders: (res, file) => {
|
|
||||||
// make sure the HTML files are always revalidated
|
|
||||||
if (extname(file) === '.html') {
|
|
||||||
res.setHeader('Cache-Control', 'public, max-age=0');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -10,7 +10,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.01',
|
note: 'SF1.01',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 36000000,
|
timeStart: 36000000,
|
||||||
@@ -20,9 +20,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -38,7 +35,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.02',
|
note: 'SF1.02',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 37500000,
|
timeStart: 37500000,
|
||||||
@@ -48,9 +45,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -66,7 +60,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.03',
|
note: 'SF1.03',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 39000000,
|
timeStart: 39000000,
|
||||||
@@ -76,9 +70,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -94,7 +85,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.04',
|
note: 'SF1.04',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 40500000,
|
timeStart: 40500000,
|
||||||
@@ -104,9 +95,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -122,7 +110,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.05',
|
note: 'SF1.05',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 42000000,
|
timeStart: 42000000,
|
||||||
@@ -132,9 +120,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -155,7 +140,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.06',
|
note: 'SF1.06',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 47100000,
|
timeStart: 47100000,
|
||||||
@@ -165,9 +150,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -183,7 +165,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.07',
|
note: 'SF1.07',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 48600000,
|
timeStart: 48600000,
|
||||||
@@ -193,9 +175,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -211,7 +190,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.08',
|
note: 'SF1.08',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 50100000,
|
timeStart: 50100000,
|
||||||
@@ -221,9 +200,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -239,7 +215,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.09',
|
note: 'SF1.09',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 51600000,
|
timeStart: 51600000,
|
||||||
@@ -249,9 +225,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -267,7 +240,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.10',
|
note: 'SF1.10',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 53100000,
|
timeStart: 53100000,
|
||||||
@@ -277,9 +250,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -300,7 +270,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.11',
|
note: 'SF1.11',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 56100000,
|
timeStart: 56100000,
|
||||||
@@ -310,9 +280,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -328,7 +295,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.12',
|
note: 'SF1.12',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 57600000,
|
timeStart: 57600000,
|
||||||
@@ -338,9 +305,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -356,7 +320,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.13',
|
note: 'SF1.13',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 59100000,
|
timeStart: 59100000,
|
||||||
@@ -366,9 +330,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
@@ -384,7 +345,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
note: 'SF1.14',
|
note: 'SF1.14',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
timeStart: 60600000,
|
timeStart: 60600000,
|
||||||
@@ -394,9 +355,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 500000,
|
timeWarning: 500000,
|
||||||
timeDanger: 100000,
|
timeDanger: 100000,
|
||||||
custom: {
|
custom: {
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import {
|
|||||||
TimerType,
|
TimerType,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
export const event: Omit<OntimeEvent, 'id' | 'cue'> = {
|
export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
|
||||||
title: '',
|
title: '',
|
||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
timeStrategy: TimeStrategy.LockDuration,
|
timeStrategy: TimeStrategy.LockDuration,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStart: 0,
|
timeStart: 0,
|
||||||
timeEnd: 0,
|
timeEnd: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
@@ -24,9 +24,6 @@ export const event: Omit<OntimeEvent, 'id' | 'cue'> = {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
custom: {},
|
custom: {},
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
|
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from 'ontime-utils';
|
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||||
|
|
||||||
import { loadRoll } from '../rollUtils.js';
|
import { loadRoll } from '../rollUtils.js';
|
||||||
import { prepareTimedEvents, makeOntimeEvent } from '../rundown-service/__mocks__/rundown.mocks.js';
|
|
||||||
|
const baseEvent = {
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
skip: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeOntimeEvent(patch: Partial<OntimeEvent>): OntimeEvent {
|
||||||
|
return {
|
||||||
|
...baseEvent,
|
||||||
|
...patch,
|
||||||
|
} as OntimeEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareTimedEvents(events: Partial<OntimeEvent>[]): OntimeEvent[] {
|
||||||
|
return events.map(makeOntimeEvent);
|
||||||
|
}
|
||||||
|
|
||||||
describe('loadRoll()', () => {
|
describe('loadRoll()', () => {
|
||||||
const eventlist = [
|
const eventlist = [
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ describe('getExpectedFinish()', () => {
|
|||||||
const state = {
|
const state = {
|
||||||
eventNow: {
|
eventNow: {
|
||||||
timeEnd: 30,
|
timeEnd: 30,
|
||||||
countToEnd: true,
|
isTimeToEnd: true,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 10,
|
addedTime: 10,
|
||||||
@@ -195,7 +195,7 @@ describe('getExpectedFinish()', () => {
|
|||||||
const state = {
|
const state = {
|
||||||
eventNow: {
|
eventNow: {
|
||||||
timeEnd: 600000, // 00:10:00
|
timeEnd: 600000, // 00:10:00
|
||||||
countToEnd: true,
|
isTimeToEnd: true,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
@@ -346,12 +346,12 @@ describe('getCurrent()', () => {
|
|||||||
expect(current).toBe(35);
|
expect(current).toBe(35);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on timers of type count-to-end', () => {
|
describe('on timers of type time-to-end', () => {
|
||||||
it('current time is the time to end even if it hasnt started, this is weird, but by design', () => {
|
it('current time is the time to end even if it hasnt started, this is weird, but by design', () => {
|
||||||
const state = {
|
const state = {
|
||||||
eventNow: {
|
eventNow: {
|
||||||
timeEnd: 100,
|
timeEnd: 100,
|
||||||
countToEnd: true,
|
isTimeToEnd: true,
|
||||||
},
|
},
|
||||||
clock: 30,
|
clock: 30,
|
||||||
timer: {
|
timer: {
|
||||||
@@ -376,7 +376,7 @@ describe('getCurrent()', () => {
|
|||||||
const state = {
|
const state = {
|
||||||
eventNow: {
|
eventNow: {
|
||||||
timeEnd: 100,
|
timeEnd: 100,
|
||||||
countToEnd: true,
|
isTimeToEnd: true,
|
||||||
},
|
},
|
||||||
clock: 30,
|
clock: 30,
|
||||||
timer: {
|
timer: {
|
||||||
@@ -401,7 +401,7 @@ describe('getCurrent()', () => {
|
|||||||
const state = {
|
const state = {
|
||||||
eventNow: {
|
eventNow: {
|
||||||
timeEnd: 100,
|
timeEnd: 100,
|
||||||
countToEnd: true,
|
isTimeToEnd: true,
|
||||||
},
|
},
|
||||||
clock: 30,
|
clock: 30,
|
||||||
timer: {
|
timer: {
|
||||||
@@ -427,7 +427,7 @@ describe('getCurrent()', () => {
|
|||||||
eventNow: {
|
eventNow: {
|
||||||
timeStart: 79200000, // 22:00:00
|
timeStart: 79200000, // 22:00:00
|
||||||
timeEnd: 600000, // 00:10:00
|
timeEnd: 600000, // 00:10:00
|
||||||
countToEnd: true,
|
isTimeToEnd: true,
|
||||||
},
|
},
|
||||||
clock: 79500000, // 22:05:00
|
clock: 79500000, // 22:05:00
|
||||||
timer: {
|
timer: {
|
||||||
@@ -456,7 +456,7 @@ describe('getCurrent()', () => {
|
|||||||
timeStart: 77400000, // 21:30:00
|
timeStart: 77400000, // 21:30:00
|
||||||
timeEnd: 81000000, // 22:30:00
|
timeEnd: 81000000, // 22:30:00
|
||||||
duration: 3600000, // 01:00:00
|
duration: 3600000, // 01:00:00
|
||||||
countToEnd: true,
|
isTimeToEnd: true,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
@@ -910,7 +910,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
linkStart: null,
|
linkStart: null,
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: true,
|
isTimeToEnd: true,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
note: '',
|
note: '',
|
||||||
@@ -963,7 +963,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
linkStart: null,
|
linkStart: null,
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: true,
|
isTimeToEnd: true,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
note: '',
|
note: '',
|
||||||
@@ -1014,7 +1014,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
linkStart: null,
|
linkStart: null,
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: true,
|
isTimeToEnd: true,
|
||||||
},
|
},
|
||||||
runtime: {
|
runtime: {
|
||||||
selectedEventIndex: 0,
|
selectedEventIndex: 0,
|
||||||
|
|||||||
@@ -2,21 +2,12 @@ import { MessageState, runtimeStorePlaceholder } from 'ontime-types';
|
|||||||
import { DeepPartial } from 'ts-essentials';
|
import { DeepPartial } from 'ts-essentials';
|
||||||
|
|
||||||
import { throttle } from '../../utils/throttle.js';
|
import { throttle } from '../../utils/throttle.js';
|
||||||
import type { StoreGetter, PublishFn } from '../../stores/EventStore.js';
|
import { eventStore, type PublishFn } from '../../stores/EventStore.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a throttled version of the set function
|
* Create a throttled version of the set function
|
||||||
*/
|
*/
|
||||||
let throttledSet: PublishFn = () => undefined;
|
const throttledSet: PublishFn = throttle(eventStore.set, 100);
|
||||||
let storeGet: StoreGetter = (_key: string) => undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows providing store interfaces
|
|
||||||
*/
|
|
||||||
export function init(storeSetter: PublishFn, storeGetter: StoreGetter) {
|
|
||||||
throttledSet = throttle(storeSetter, 100);
|
|
||||||
storeGet = storeGetter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exposes function to reset the internal state
|
* Exposes function to reset the internal state
|
||||||
@@ -31,7 +22,7 @@ export function clear() {
|
|||||||
* Exposes the internal state of the message service
|
* Exposes the internal state of the message service
|
||||||
*/
|
*/
|
||||||
export function getState(): MessageState {
|
export function getState(): MessageState {
|
||||||
return storeGet('message');
|
return eventStore.get('message');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,32 +2,31 @@ import * as messageService from '../MessageService.js';
|
|||||||
|
|
||||||
describe('MessageService', () => {
|
describe('MessageService', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// at runtime, the store is instantiated before the message service
|
|
||||||
const store = {};
|
|
||||||
const storeSetter = (key, value) => (store[key] = value);
|
|
||||||
const storeGetter = (key) => store[key];
|
|
||||||
messageService.init(storeSetter, storeGetter);
|
|
||||||
messageService.clear();
|
messageService.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should patch the message state', () => {
|
it('should patch the message state', () => {
|
||||||
const newState = messageService.patch({
|
const message = {
|
||||||
timer: { text: 'new text', visible: true },
|
timer: { text: 'new text', visible: true },
|
||||||
external: 'external',
|
external: 'external',
|
||||||
});
|
};
|
||||||
|
|
||||||
expect(newState).toMatchObject({
|
const newState = messageService.patch(message);
|
||||||
|
|
||||||
|
expect(newState).toEqual({
|
||||||
timer: { text: 'new text', visible: true, blackout: false, blink: false, secondarySource: null },
|
timer: { text: 'new text', visible: true, blackout: false, blink: false, secondarySource: null },
|
||||||
external: 'external',
|
external: 'external',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not affect other properties when patching', () => {
|
it('should not affect other properties when patching', () => {
|
||||||
const newState = messageService.patch({
|
const initialMessage = {
|
||||||
timer: { text: 'initial text', visible: true },
|
timer: { text: 'initial text', visible: true },
|
||||||
});
|
};
|
||||||
|
|
||||||
expect(newState).toMatchObject({
|
const newState = messageService.patch(initialMessage);
|
||||||
|
|
||||||
|
expect(newState).toEqual({
|
||||||
timer: { text: 'initial text', visible: true, blackout: false, blink: false, secondarySource: null },
|
timer: { text: 'initial text', visible: true, blackout: false, blink: false, secondarySource: null },
|
||||||
external: '',
|
external: '',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { SupportedEvent, OntimeEvent, OntimeDelay } from 'ontime-types';
|
|
||||||
|
|
||||||
const baseEvent = {
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
skip: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to create a Ontime event
|
|
||||||
*/
|
|
||||||
export function makeOntimeEvent(patch: Partial<OntimeEvent>): OntimeEvent {
|
|
||||||
return {
|
|
||||||
...baseEvent,
|
|
||||||
...patch,
|
|
||||||
} as OntimeEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to create a delay event
|
|
||||||
*/
|
|
||||||
export function makeOntimeDelay(duration: number): OntimeDelay {
|
|
||||||
return { id: 'delay', type: SupportedEvent.Delay, duration };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to generate a rundown of OntimeEvents form partial objects
|
|
||||||
*/
|
|
||||||
export function prepareTimedEvents(events: Partial<OntimeEvent>[]): OntimeEvent[] {
|
|
||||||
return events.map(makeOntimeEvent);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,20 @@
|
|||||||
import { OntimeBlock, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
import { OntimeBlock, OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||||
|
import { apply } from '../delayUtils.js';
|
||||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
|
|
||||||
import { apply } from '../delayUtils.js';
|
/**
|
||||||
import { makeOntimeDelay, makeOntimeEvent } from '../__mocks__/rundown.mocks.js';
|
* Small utility to fill in the necessary data for the test
|
||||||
|
*/
|
||||||
|
function makeOntimeEvent(event: Partial<OntimeEvent>): OntimeEvent {
|
||||||
|
return { ...event, type: SupportedEvent.Event, revision: 1 } as OntimeEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small utility to make a delay event
|
||||||
|
*/
|
||||||
|
function makeOntimeDelay(duration: number): OntimeDelay {
|
||||||
|
return { id: 'delay', type: SupportedEvent.Delay, duration } as OntimeDelay;
|
||||||
|
}
|
||||||
|
|
||||||
describe('apply()', () => {
|
describe('apply()', () => {
|
||||||
it('applies a positive delay to the rundown', () => {
|
it('applies a positive delay to the rundown', () => {
|
||||||
@@ -138,11 +150,11 @@ describe('apply()', () => {
|
|||||||
makeOntimeDelay(100),
|
makeOntimeDelay(100),
|
||||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||||
// gap 50
|
// gap 50
|
||||||
makeOntimeEvent({ id: '2', timeStart: 150, timeEnd: 200, duration: 50, gap: 50 }),
|
makeOntimeEvent({ id: '2', timeStart: 150, timeEnd: 200, duration: 50 }),
|
||||||
// gap 0
|
|
||||||
makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 250, duration: 50, gap: 0 }),
|
|
||||||
// gap 50
|
// gap 50
|
||||||
makeOntimeEvent({ id: '4', timeStart: 300, timeEnd: 350, duration: 50, gap: 50 }),
|
makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 250, duration: 50 }),
|
||||||
|
// gap 50
|
||||||
|
makeOntimeEvent({ id: '4', timeStart: 300, timeEnd: 350, duration: 50 }),
|
||||||
// linked
|
// linked
|
||||||
makeOntimeEvent({ id: '5', timeStart: 350, timeEnd: 400, duration: 50, linkStart: '4' }),
|
makeOntimeEvent({ id: '5', timeStart: 350, timeEnd: 400, duration: 50, linkStart: '4' }),
|
||||||
];
|
];
|
||||||
@@ -153,7 +165,7 @@ describe('apply()', () => {
|
|||||||
// gap 50 (100 - 50)
|
// gap 50 (100 - 50)
|
||||||
{ id: '2', timeStart: 150 + 50, timeEnd: 200 + 50, duration: 50, revision: 2 },
|
{ id: '2', timeStart: 150 + 50, timeEnd: 200 + 50, duration: 50, revision: 2 },
|
||||||
// gap 50 (50 - 50)
|
// gap 50 (50 - 50)
|
||||||
{ id: '3', timeStart: 200 + 50, timeEnd: 250 + 50, duration: 50, revision: 2, gap: 0 },
|
{ id: '3', timeStart: 200 + 50, timeEnd: 250 + 50, duration: 50, revision: 2 },
|
||||||
// gap (delay is 0)
|
// gap (delay is 0)
|
||||||
{ id: '4', timeStart: 300, timeEnd: 350, duration: 50, revision: 1 },
|
{ id: '4', timeStart: 300, timeEnd: 350, duration: 50, revision: 1 },
|
||||||
// linked
|
// linked
|
||||||
@@ -166,8 +178,6 @@ describe('apply()', () => {
|
|||||||
makeOntimeDelay(2 * MILLIS_PER_HOUR),
|
makeOntimeDelay(2 * MILLIS_PER_HOUR),
|
||||||
makeOntimeEvent({
|
makeOntimeEvent({
|
||||||
id: '1',
|
id: '1',
|
||||||
gap: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
timeStart: 46800000, // 13:00:00
|
timeStart: 46800000, // 13:00:00
|
||||||
timeEnd: 50400000, // 14:00:00
|
timeEnd: 50400000, // 14:00:00
|
||||||
duration: MILLIS_PER_HOUR,
|
duration: MILLIS_PER_HOUR,
|
||||||
@@ -175,8 +185,6 @@ describe('apply()', () => {
|
|||||||
// gap 1h
|
// gap 1h
|
||||||
makeOntimeEvent({
|
makeOntimeEvent({
|
||||||
id: '2',
|
id: '2',
|
||||||
gap: 1 * MILLIS_PER_HOUR,
|
|
||||||
dayOffset: 0,
|
|
||||||
timeStart: 54000000, // 15:00:00
|
timeStart: 54000000, // 15:00:00
|
||||||
timeEnd: 57600000, // 16:00:00
|
timeEnd: 57600000, // 16:00:00
|
||||||
duration: MILLIS_PER_HOUR,
|
duration: MILLIS_PER_HOUR,
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ describe('add() mutation', () => {
|
|||||||
test('adds an event to the rundown', () => {
|
test('adds an event to the rundown', () => {
|
||||||
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
||||||
const testRundown: OntimeRundown = [];
|
const testRundown: OntimeRundown = [];
|
||||||
const { newRundown } = add({ atIndex: 0, event: mockEvent, rundown: testRundown });
|
const { newRundown } = add({ atIndex: 0, event: mockEvent, persistedRundown: testRundown });
|
||||||
expect(newRundown.length).toBe(1);
|
expect(newRundown.length).toBe(1);
|
||||||
expect(newRundown[0]).toMatchObject(mockEvent);
|
expect(newRundown[0]).toMatchObject(mockEvent);
|
||||||
});
|
});
|
||||||
@@ -448,7 +448,7 @@ describe('remove() mutation', () => {
|
|||||||
test('deletes an event from the rundown', () => {
|
test('deletes an event from the rundown', () => {
|
||||||
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
||||||
const testRundown: OntimeRundown = [mockEvent];
|
const testRundown: OntimeRundown = [mockEvent];
|
||||||
const { newRundown } = remove({ eventIds: [mockEvent.id], rundown: testRundown });
|
const { newRundown } = remove({ eventIds: [mockEvent.id], persistedRundown: testRundown });
|
||||||
expect(newRundown.length).toBe(0);
|
expect(newRundown.length).toBe(0);
|
||||||
});
|
});
|
||||||
test('deletes multiple events from the rundown', () => {
|
test('deletes multiple events from the rundown', () => {
|
||||||
@@ -460,7 +460,7 @@ describe('remove() mutation', () => {
|
|||||||
{ type: SupportedEvent.Event, id: '5' } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '5' } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Event, id: '6' } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '6' } as OntimeEvent,
|
||||||
];
|
];
|
||||||
const { newRundown } = remove({ eventIds: ['1', '2', '3'], rundown: testRundown });
|
const { newRundown } = remove({ eventIds: ['1', '2', '3'], persistedRundown: testRundown });
|
||||||
expect(newRundown.length).toBe(3);
|
expect(newRundown.length).toBe(3);
|
||||||
expect(newRundown.at(0)?.id).toBe('4');
|
expect(newRundown.at(0)?.id).toBe('4');
|
||||||
});
|
});
|
||||||
@@ -474,7 +474,7 @@ describe('edit() mutation', () => {
|
|||||||
const { newRundown, newEvent } = edit({
|
const { newRundown, newEvent } = edit({
|
||||||
eventId: mockEvent.id,
|
eventId: mockEvent.id,
|
||||||
patch: mockEventPatch,
|
patch: mockEventPatch,
|
||||||
rundown: testRundown,
|
persistedRundown: testRundown,
|
||||||
});
|
});
|
||||||
expect(newRundown.length).toBe(1);
|
expect(newRundown.length).toBe(1);
|
||||||
expect(newEvent).toMatchObject({
|
expect(newEvent).toMatchObject({
|
||||||
@@ -487,7 +487,7 @@ describe('edit() mutation', () => {
|
|||||||
|
|
||||||
describe('batchEdit() mutation', () => {
|
describe('batchEdit() mutation', () => {
|
||||||
it('should correctly apply the patch to the events with the given IDs', () => {
|
it('should correctly apply the patch to the events with the given IDs', () => {
|
||||||
const testRundown: OntimeRundown = [
|
const persistedRundown: OntimeRundown = [
|
||||||
{ id: '1', type: SupportedEvent.Event, cue: 'data1' } as OntimeEvent,
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1' } as OntimeEvent,
|
||||||
{ id: '2', type: SupportedEvent.Event, cue: 'data2' } as OntimeEvent,
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2' } as OntimeEvent,
|
||||||
{ id: '3', type: SupportedEvent.Event, cue: 'data3' } as OntimeEvent,
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3' } as OntimeEvent,
|
||||||
@@ -495,7 +495,7 @@ describe('batchEdit() mutation', () => {
|
|||||||
const eventIds = ['1', '3'];
|
const eventIds = ['1', '3'];
|
||||||
const patch = { cue: 'newData' };
|
const patch = { cue: 'newData' };
|
||||||
|
|
||||||
const { newRundown } = batchEdit({ rundown: testRundown, eventIds, patch });
|
const { newRundown } = batchEdit({ persistedRundown, eventIds, patch });
|
||||||
|
|
||||||
expect(newRundown).toMatchObject([
|
expect(newRundown).toMatchObject([
|
||||||
{ id: '1', type: SupportedEvent.Event, cue: 'newData' },
|
{ id: '1', type: SupportedEvent.Event, cue: 'newData' },
|
||||||
@@ -507,16 +507,16 @@ describe('batchEdit() mutation', () => {
|
|||||||
|
|
||||||
describe('reorder() mutation', () => {
|
describe('reorder() mutation', () => {
|
||||||
it('should correctly reorder two events', () => {
|
it('should correctly reorder two events', () => {
|
||||||
const testRundown: OntimeRundown = [
|
const persistedRundown: OntimeRundown = [
|
||||||
{ id: '1', type: SupportedEvent.Event, cue: 'data1', revision: 0 } as OntimeEvent,
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1', revision: 0 } as OntimeEvent,
|
||||||
{ id: '2', type: SupportedEvent.Event, cue: 'data2', revision: 0 } as OntimeEvent,
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2', revision: 0 } as OntimeEvent,
|
||||||
{ id: '3', type: SupportedEvent.Event, cue: 'data3', revision: 0 } as OntimeEvent,
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3', revision: 0 } as OntimeEvent,
|
||||||
];
|
];
|
||||||
const { newRundown } = reorder({
|
const { newRundown } = reorder({
|
||||||
rundown: testRundown,
|
persistedRundown,
|
||||||
eventId: testRundown[0].id,
|
eventId: persistedRundown[0].id,
|
||||||
from: 0,
|
from: 0,
|
||||||
to: testRundown.length - 1,
|
to: persistedRundown.length - 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(newRundown).toMatchObject([
|
expect(newRundown).toMatchObject([
|
||||||
@@ -529,15 +529,15 @@ describe('reorder() mutation', () => {
|
|||||||
|
|
||||||
describe('swap() mutation', () => {
|
describe('swap() mutation', () => {
|
||||||
it('should correctly swap data between events', () => {
|
it('should correctly swap data between events', () => {
|
||||||
const testRundown: OntimeRundown = [
|
const persistedRundown: OntimeRundown = [
|
||||||
{ id: '1', type: SupportedEvent.Event, cue: 'data1', timeStart: 1, revision: 0 } as OntimeEvent,
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1', timeStart: 1, revision: 0 } as OntimeEvent,
|
||||||
{ id: '2', type: SupportedEvent.Event, cue: 'data2', timeStart: 2, revision: 0 } as OntimeEvent,
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2', timeStart: 2, revision: 0 } as OntimeEvent,
|
||||||
{ id: '3', type: SupportedEvent.Event, cue: 'data3', timeStart: 3, revision: 0 } as OntimeEvent,
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3', timeStart: 3, revision: 0 } as OntimeEvent,
|
||||||
];
|
];
|
||||||
const { newRundown } = swap({
|
const { newRundown } = swap({
|
||||||
rundown: testRundown,
|
persistedRundown,
|
||||||
fromId: testRundown[0].id,
|
fromId: persistedRundown[0].id,
|
||||||
toId: testRundown[1].id,
|
toId: persistedRundown[1].id,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect((newRundown[0] as OntimeEvent).id).toBe('1');
|
expect((newRundown[0] as OntimeEvent).id).toBe('1');
|
||||||
@@ -565,7 +565,7 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 600000,
|
timeStart: 600000,
|
||||||
@@ -576,9 +576,6 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: '659e1',
|
id: '659e1',
|
||||||
@@ -595,7 +592,7 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 1200000,
|
timeStart: 1200000,
|
||||||
@@ -606,9 +603,6 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: '1c48f',
|
id: '1c48f',
|
||||||
@@ -625,7 +619,7 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 600000,
|
timeStart: 600000,
|
||||||
@@ -636,9 +630,6 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: 'd48c2',
|
id: 'd48c2',
|
||||||
@@ -655,7 +646,7 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 1200000,
|
timeStart: 1200000,
|
||||||
@@ -666,9 +657,6 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: '2f185',
|
id: '2f185',
|
||||||
@@ -694,7 +682,7 @@ describe('getDelayAt()', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 600000,
|
timeStart: 600000,
|
||||||
@@ -709,8 +697,6 @@ describe('getDelayAt()', () => {
|
|||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: '659e1',
|
id: '659e1',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
cue: '1',
|
cue: '1',
|
||||||
custom: {},
|
custom: {},
|
||||||
},
|
},
|
||||||
@@ -724,7 +710,7 @@ describe('getDelayAt()', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 1200000,
|
timeStart: 1200000,
|
||||||
@@ -735,8 +721,6 @@ describe('getDelayAt()', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: '1c48f',
|
id: '1c48f',
|
||||||
@@ -754,7 +738,7 @@ describe('getDelayAt()', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 600000,
|
timeStart: 600000,
|
||||||
@@ -765,8 +749,6 @@ describe('getDelayAt()', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: 'd48c2',
|
id: 'd48c2',
|
||||||
@@ -784,7 +766,7 @@ describe('getDelayAt()', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 1200000,
|
timeStart: 1200000,
|
||||||
@@ -795,8 +777,6 @@ describe('getDelayAt()', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: '2f185',
|
id: '2f185',
|
||||||
@@ -840,7 +820,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 600000,
|
timeStart: 600000,
|
||||||
@@ -851,8 +831,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: '659e1',
|
id: '659e1',
|
||||||
@@ -870,7 +848,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 1200000,
|
timeStart: 1200000,
|
||||||
@@ -881,8 +859,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: '1c48f',
|
id: '1c48f',
|
||||||
@@ -900,7 +876,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 600000,
|
timeStart: 600000,
|
||||||
@@ -911,8 +887,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: 'd48c2',
|
id: 'd48c2',
|
||||||
@@ -930,7 +904,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
note: '',
|
note: '',
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: false,
|
isTimeToEnd: false,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: null,
|
linkStart: null,
|
||||||
timeStart: 1200000,
|
timeStart: 1200000,
|
||||||
@@ -941,8 +915,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
id: '2f185',
|
id: '2f185',
|
||||||
@@ -963,7 +935,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
|
|
||||||
describe('custom fields', () => {
|
describe('custom fields', () => {
|
||||||
describe('createCustomField()', () => {
|
describe('createCustomField()', () => {
|
||||||
it('creates a field from given parameters', () => {
|
it('creates a field from given parameters', async () => {
|
||||||
const expected = {
|
const expected = {
|
||||||
Lighting: {
|
Lighting: {
|
||||||
label: 'Lighting',
|
label: 'Lighting',
|
||||||
@@ -972,14 +944,14 @@ describe('custom fields', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const customField = createCustomField({ label: 'Lighting', type: 'string', colour: 'blue' });
|
const customField = await createCustomField({ label: 'Lighting', type: 'string', colour: 'blue' });
|
||||||
expect(customField).toStrictEqual(expected);
|
expect(customField).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('editCustomField()', () => {
|
describe('editCustomField()', () => {
|
||||||
it('edits a field with a given label', () => {
|
it('edits a field with a given label', async () => {
|
||||||
createCustomField({ label: 'Sound', type: 'string', colour: 'blue' });
|
await createCustomField({ label: 'Sound', type: 'string', colour: 'blue' });
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
Lighting: {
|
Lighting: {
|
||||||
@@ -994,14 +966,14 @@ describe('custom fields', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const customField = editCustomField('Sound', { label: 'Sound', type: 'string', colour: 'green' });
|
const customField = await editCustomField('Sound', { label: 'Sound', type: 'string', colour: 'green' });
|
||||||
expect(customFieldChangelog).toStrictEqual(new Map());
|
expect(customFieldChangelog).toStrictEqual(new Map());
|
||||||
|
|
||||||
expect(customField).toStrictEqual(expected);
|
expect(customField).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renames a field to a new label', () => {
|
it('renames a field to a new label', async () => {
|
||||||
const created = createCustomField({ label: 'Video', type: 'string', colour: 'red' });
|
const created = await createCustomField({ label: 'Video', type: 'string', colour: 'red' });
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
Lighting: {
|
Lighting: {
|
||||||
@@ -1043,10 +1015,10 @@ describe('custom fields', () => {
|
|||||||
|
|
||||||
// We need to flush all scheduled tasks for the generate function to settle
|
// We need to flush all scheduled tasks for the generate function to settle
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const customField = editCustomField('Video', { label: 'AV', type: 'string', colour: 'red' });
|
const customField = await editCustomField('Video', { label: 'AV', type: 'string', colour: 'red' });
|
||||||
expect(customField).toStrictEqual(expectedAfter);
|
expect(customField).toStrictEqual(expectedAfter);
|
||||||
expect(customFieldChangelog).toStrictEqual(new Map([['Video', 'AV']]));
|
expect(customFieldChangelog).toStrictEqual(new Map([['Video', 'AV']]));
|
||||||
editCustomField('AV', { label: 'Video' });
|
await editCustomField('AV', { label: 'Video' });
|
||||||
vi.runAllTimers();
|
vi.runAllTimers();
|
||||||
expect(customFieldChangelog).toStrictEqual(new Map());
|
expect(customFieldChangelog).toStrictEqual(new Map());
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
@@ -1054,7 +1026,7 @@ describe('custom fields', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('removeCustomField()', () => {
|
describe('removeCustomField()', () => {
|
||||||
it('deletes a field with a given label', () => {
|
it('deletes a field with a given label', async () => {
|
||||||
const expected = {
|
const expected = {
|
||||||
Lighting: {
|
Lighting: {
|
||||||
label: 'Lighting',
|
label: 'Lighting',
|
||||||
@@ -1068,7 +1040,7 @@ describe('custom fields', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const customField = removeCustomField('Sound');
|
const customField = await removeCustomField('Sound');
|
||||||
|
|
||||||
expect(customField).toStrictEqual(expected);
|
expect(customField).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import {
|
|||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import {
|
import {
|
||||||
addToCustomAssignment,
|
addToCustomAssignment,
|
||||||
calculateDayOffset,
|
|
||||||
getLink,
|
getLink,
|
||||||
handleCustomField,
|
handleCustomField,
|
||||||
handleLink,
|
handleLink,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
isDataStale,
|
isDataStale,
|
||||||
} from '../rundownCacheUtils.js';
|
} from '../rundownCacheUtils.js';
|
||||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
|
||||||
|
|
||||||
describe('getLink()', () => {
|
describe('getLink()', () => {
|
||||||
it('should return null if there is no link', () => {
|
it('should return null if there is no link', () => {
|
||||||
@@ -249,61 +247,3 @@ describe('hasChanges()', () => {
|
|||||||
expect(hasChanges(existing, newEvent)).toBe(false);
|
expect(hasChanges(existing, newEvent)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('calculateDayOffset', () => {
|
|
||||||
it('returns 0 if there is no previous event', () => {
|
|
||||||
expect(calculateDayOffset({ timeStart: 0 })).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 0 if the previous event duration is 0', () => {
|
|
||||||
expect(calculateDayOffset({ timeStart: 0 }, { timeStart: 0, duration: 0 })).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 0 if event starts after previous', () => {
|
|
||||||
expect(calculateDayOffset({ timeStart: 11 }, { timeStart: 10, duration: 2 })).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 1 if event starts before previous', () => {
|
|
||||||
expect(calculateDayOffset({ timeStart: 9 }, { timeStart: 10, duration: 2 })).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 1 if event starts at the same time as one before', () => {
|
|
||||||
expect(calculateDayOffset({ timeStart: 10 }, { timeStart: 10, duration: 2 })).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should account for an event that crossed midnight and there is a overlap', () => {
|
|
||||||
expect(
|
|
||||||
calculateDayOffset(
|
|
||||||
{ timeStart: MILLIS_PER_HOUR }, // starts at 01:00:00
|
|
||||||
{ timeStart: 20 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 02:00:00
|
|
||||||
),
|
|
||||||
).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should account for an event that crossed midnight and there is a gap', () => {
|
|
||||||
expect(
|
|
||||||
calculateDayOffset(
|
|
||||||
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
|
|
||||||
{ timeStart: 23 * MILLIS_PER_HOUR, duration: 2 * MILLIS_PER_HOUR }, // ends at 01:00:00
|
|
||||||
),
|
|
||||||
).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should account for an event that crossed midnight with no overlaps or gaps', () => {
|
|
||||||
expect(
|
|
||||||
calculateDayOffset(
|
|
||||||
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
|
|
||||||
{ timeStart: 20 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 02:00:00
|
|
||||||
),
|
|
||||||
).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should account for an event that finishes exactly at midnight', () => {
|
|
||||||
expect(
|
|
||||||
calculateDayOffset(
|
|
||||||
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
|
|
||||||
{ timeStart: 23 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 24:00:00
|
|
||||||
),
|
|
||||||
).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OntimeRundown, isOntimeDelay, isOntimeBlock, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
import { OntimeRundown, isOntimeDelay, isOntimeBlock, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||||
import { deleteAtIndex } from 'ontime-utils';
|
import { getTimeFromPrevious, deleteAtIndex } from 'ontime-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates all delays in a given rundown
|
* Calculates all delays in a given rundown
|
||||||
@@ -133,14 +133,21 @@ export function apply(eventId: string, rundown: OntimeRundown): OntimeRundown {
|
|||||||
|
|
||||||
// if the event is not linked, we try and maintain gaps
|
// if the event is not linked, we try and maintain gaps
|
||||||
if (lastEntry !== null) {
|
if (lastEntry !== null) {
|
||||||
|
const timeFromPrevious: number = getTimeFromPrevious(
|
||||||
|
currentEntry.timeStart,
|
||||||
|
lastEntry.timeStart,
|
||||||
|
lastEntry.timeEnd,
|
||||||
|
lastEntry.duration,
|
||||||
|
);
|
||||||
|
|
||||||
// when applying negative delays, we need to unlink the event
|
// when applying negative delays, we need to unlink the event
|
||||||
// if the previous event was fully consumed by the delay
|
// if the previous event was fully consumed by the delay
|
||||||
if (currentEntry.linkStart && delayValue < 0 && lastEntry.timeStart + delayValue < 0) {
|
if (currentEntry.linkStart && delayValue < 0 && lastEntry.timeStart + delayValue < 0) {
|
||||||
shouldUnlink = true;
|
shouldUnlink = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentEntry.gap > 0) {
|
if (timeFromPrevious > 0) {
|
||||||
delayValue = Math.max(delayValue - currentEntry.gap, 0);
|
delayValue = Math.max(delayValue - timeFromPrevious, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delayValue === 0) {
|
if (delayValue === 0) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
CustomField,
|
CustomField,
|
||||||
CustomFieldLabel,
|
CustomFieldLabel,
|
||||||
CustomFields,
|
CustomFields,
|
||||||
isOntimeBlock,
|
|
||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
isPlayableEvent,
|
isPlayableEvent,
|
||||||
@@ -24,7 +23,7 @@ import {
|
|||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { createPatch } from '../../utils/parser.js';
|
import { createPatch } from '../../utils/parser.js';
|
||||||
import { apply } from './delayUtils.js';
|
import { apply } from './delayUtils.js';
|
||||||
import { calculateDayOffset, handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
|
import { handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
|
||||||
|
|
||||||
type EventID = string;
|
type EventID = string;
|
||||||
type NormalisedRundown = Record<EventID, OntimeRundownEntry>;
|
type NormalisedRundown = Record<EventID, OntimeRundownEntry>;
|
||||||
@@ -32,30 +31,16 @@ type NormalisedRundown = Record<EventID, OntimeRundownEntry>;
|
|||||||
let persistedRundown: OntimeRundown = [];
|
let persistedRundown: OntimeRundown = [];
|
||||||
let persistedCustomFields: CustomFields = {};
|
let persistedCustomFields: CustomFields = {};
|
||||||
|
|
||||||
/**
|
/** Utility function gets to expose data */
|
||||||
* Get the cached rundown without triggering regeneration
|
|
||||||
*/
|
|
||||||
export const getPersistedRundown = (): OntimeRundown => persistedRundown;
|
export const getPersistedRundown = (): OntimeRundown => persistedRundown;
|
||||||
export const getCustomFields = (): CustomFields => persistedCustomFields;
|
export const getCustomFields = (): CustomFields => persistedCustomFields;
|
||||||
|
|
||||||
let normalisedRundown: NormalisedRundown = {};
|
let rundown: NormalisedRundown = {};
|
||||||
let order: EventID[] = [];
|
let order: EventID[] = [];
|
||||||
let revision = 0;
|
let revision = 0;
|
||||||
|
|
||||||
/**
|
|
||||||
* all mutating functions will set this value if there is a need for re-generation
|
|
||||||
* but will only be cleared by the generate function
|
|
||||||
*/
|
|
||||||
let isStale = true;
|
let isStale = true;
|
||||||
|
|
||||||
/** Allows safely setting the stale state without accidentally clearing it */
|
|
||||||
function setIsStale() {
|
|
||||||
isStale = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalDelay = 0;
|
let totalDelay = 0;
|
||||||
let totalDuration = 0;
|
let totalDuration = 0;
|
||||||
let totalDays = 0;
|
|
||||||
let firstStart: MaybeNumber = null;
|
let firstStart: MaybeNumber = null;
|
||||||
let lastEnd: MaybeNumber = null;
|
let lastEnd: MaybeNumber = null;
|
||||||
|
|
||||||
@@ -87,49 +72,39 @@ export async function init(initialRundown: Readonly<OntimeRundown>, customFields
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility generate cache
|
* Utility initialises cache
|
||||||
* @private should not be called outside of `rundownCache.ts`
|
* @param rundown
|
||||||
*/
|
*/
|
||||||
export function generate(
|
export function generate(
|
||||||
initialRundown: OntimeRundown = persistedRundown,
|
initialRundown: OntimeRundown = persistedRundown,
|
||||||
customFields: CustomFields = persistedCustomFields,
|
customFields: CustomFields = persistedCustomFields,
|
||||||
) {
|
) {
|
||||||
function clearIsStale() {
|
|
||||||
isStale = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we decided to re-write this dataset for every change
|
// we decided to re-write this dataset for every change
|
||||||
// instead of maintaining logic to update it
|
// instead of maintaining logic to update it
|
||||||
|
|
||||||
assignedCustomFields = {};
|
assignedCustomFields = {};
|
||||||
normalisedRundown = {};
|
rundown = {};
|
||||||
order = [];
|
order = [];
|
||||||
links = {};
|
links = {};
|
||||||
firstStart = null;
|
firstStart = null;
|
||||||
lastEnd = null;
|
lastEnd = null;
|
||||||
totalDuration = 0;
|
totalDuration = 0;
|
||||||
totalDays = 0;
|
|
||||||
totalDelay = 0;
|
totalDelay = 0;
|
||||||
|
|
||||||
let lastEntry: PlayableEvent | null = null;
|
let lastEntry: PlayableEvent | null = null;
|
||||||
|
|
||||||
for (let i = 0; i < initialRundown.length; i++) {
|
for (let i = 0; i < initialRundown.length; i++) {
|
||||||
|
// TODO: filter properties that should not be persisted (eg: delay)
|
||||||
// we assign a reference to the current entry, this will be mutated in place
|
// we assign a reference to the current entry, this will be mutated in place
|
||||||
const currentEntry = initialRundown[i];
|
const currentEntry = initialRundown[i];
|
||||||
|
|
||||||
if (isOntimeEvent(currentEntry)) {
|
if (isOntimeEvent(currentEntry)) {
|
||||||
currentEntry.delay = 0;
|
|
||||||
currentEntry.gap = 0;
|
|
||||||
|
|
||||||
// 1. handle links - mutates updatedEvent
|
// 1. handle links - mutates updatedEvent
|
||||||
handleLink(i, initialRundown, currentEntry, links);
|
handleLink(i, initialRundown, currentEntry, links);
|
||||||
|
|
||||||
// 2. handle custom fields - mutates updatedEvent
|
// 2. handle custom fields - mutates updatedEvent
|
||||||
handleCustomField(customFields, customFieldChangelog, currentEntry, assignedCustomFields);
|
handleCustomField(customFields, customFieldChangelog, currentEntry, assignedCustomFields);
|
||||||
|
|
||||||
totalDays += calculateDayOffset(currentEntry, lastEntry);
|
|
||||||
currentEntry.dayOffset = totalDays;
|
|
||||||
|
|
||||||
// update rundown metadata, it only concerns playable events
|
// update rundown metadata, it only concerns playable events
|
||||||
if (isPlayableEvent(currentEntry)) {
|
if (isPlayableEvent(currentEntry)) {
|
||||||
// fist start is always the first event
|
// fist start is always the first event
|
||||||
@@ -137,58 +112,57 @@ export function generate(
|
|||||||
firstStart = currentEntry.timeStart;
|
firstStart = currentEntry.timeStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentEntry.gap = getTimeFromPrevious(currentEntry, lastEntry);
|
const timeFromPrevious: number = getTimeFromPrevious(
|
||||||
|
currentEntry.timeStart,
|
||||||
|
lastEntry?.timeStart,
|
||||||
|
lastEntry?.timeEnd,
|
||||||
|
lastEntry?.duration,
|
||||||
|
);
|
||||||
|
|
||||||
if (currentEntry.gap === 0) {
|
if (timeFromPrevious === 0) {
|
||||||
// event starts on previous finish, we add its duration
|
// event starts on previous finish, we add its duration
|
||||||
totalDuration += currentEntry.duration;
|
totalDuration += currentEntry.duration;
|
||||||
} else if (currentEntry.gap > 0) {
|
} else if (timeFromPrevious > 0) {
|
||||||
// event has a gap, we add the gap and the duration
|
// event has a gap, we add the gap and the duration
|
||||||
totalDuration += currentEntry.gap + currentEntry.duration;
|
totalDuration += timeFromPrevious + currentEntry.duration;
|
||||||
} else if (currentEntry.gap < 0) {
|
} else if (timeFromPrevious < 0) {
|
||||||
// there is an overlap, we remove the overlap from the duration
|
// there is an overlap, we remove the overlap from the duration
|
||||||
// ensuring that the sum is not negative (ie: fully overlapped events)
|
// ensuring that the sum is not negative (ie: fully overlapped events)
|
||||||
// NOTE: we add the gap since it is a negative number
|
// NOTE: we add the gap since it is a negative number
|
||||||
totalDuration += Math.max(currentEntry.duration + currentEntry.gap, 0);
|
totalDuration += Math.max(currentEntry.duration + timeFromPrevious, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove eventual gaps from the accumulated delay
|
// remove eventual gaps from the accumulated delay
|
||||||
// we only affect positive delays (time forwards)
|
// we only affect positive delays (time forwards)
|
||||||
if (totalDelay > 0 && currentEntry.gap > 0) {
|
if (totalDelay > 0 && timeFromPrevious > 0) {
|
||||||
totalDelay = Math.max(totalDelay - currentEntry.gap, 0);
|
totalDelay = Math.max(totalDelay - timeFromPrevious, 0);
|
||||||
}
|
}
|
||||||
// current event delay is the current accumulated delay
|
// current event delay is the current accumulated delay
|
||||||
currentEntry.delay = totalDelay;
|
currentEntry.delay = totalDelay;
|
||||||
|
|
||||||
// lastEntry is the event with the latest end time
|
// lastEntry is the event with the latest end time
|
||||||
if (isNewLatest(currentEntry, lastEntry)) {
|
if (isNewLatest(currentEntry.timeStart, currentEntry.timeEnd, lastEntry?.timeStart, lastEntry?.timeEnd)) {
|
||||||
lastEntry = currentEntry;
|
lastEntry = currentEntry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (isOntimeDelay(currentEntry)) {
|
}
|
||||||
// calculate delays
|
|
||||||
// !!! this must happen after handling the links
|
// calculate delays
|
||||||
|
// !!! this must happen after handling the links
|
||||||
|
if (isOntimeDelay(currentEntry)) {
|
||||||
totalDelay += currentEntry.duration;
|
totalDelay += currentEntry.duration;
|
||||||
} else if (isOntimeBlock(currentEntry)) {
|
|
||||||
// calculate block - nothing yet
|
|
||||||
} else {
|
|
||||||
// unknown - type skip it
|
|
||||||
// this is needed to get the type guard working when we assign the entry to the rundown
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add id to order
|
// add id to order
|
||||||
order.push(currentEntry.id);
|
order.push(currentEntry.id);
|
||||||
// add entry to rundown
|
// add entry to rundown
|
||||||
normalisedRundown[currentEntry.id] = currentEntry;
|
rundown[currentEntry.id] = currentEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastEnd = lastEntry?.timeEnd ?? null;
|
lastEnd = lastEntry?.timeEnd ?? null;
|
||||||
clearIsStale();
|
isStale = false;
|
||||||
customFieldChangelog.clear();
|
customFieldChangelog.clear();
|
||||||
|
return { rundown, order, links, totalDelay, totalDuration, assignedCustomFields };
|
||||||
//The return value is used for testing
|
|
||||||
return { rundown: normalisedRundown, order, links, totalDelay, totalDuration, assignedCustomFields };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns an ID guaranteed to be unique */
|
/** Returns an ID guaranteed to be unique */
|
||||||
@@ -199,7 +173,7 @@ export function getUniqueId(): string {
|
|||||||
let id = '';
|
let id = '';
|
||||||
do {
|
do {
|
||||||
id = generateId();
|
id = generateId();
|
||||||
} while (Object.hasOwn(normalisedRundown, id));
|
} while (Object.hasOwn(rundown, id));
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,15 +194,15 @@ type RundownCache = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the full rundown cache.
|
* Returns cached data
|
||||||
* Will triggering regeneration if data is stale.
|
* @returns {RundownCache}
|
||||||
*/
|
*/
|
||||||
export function get(): Readonly<RundownCache> {
|
export function get(): Readonly<RundownCache> {
|
||||||
if (isStale) {
|
if (isStale) {
|
||||||
generate();
|
generate();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
rundown: normalisedRundown,
|
rundown,
|
||||||
order,
|
order,
|
||||||
revision,
|
revision,
|
||||||
totalDelay,
|
totalDelay,
|
||||||
@@ -238,7 +212,6 @@ export function get(): Readonly<RundownCache> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns calculated metadata from rundown
|
* Returns calculated metadata from rundown
|
||||||
* Will triggering regeneration if data is stale.
|
|
||||||
*/
|
*/
|
||||||
export function getMetadata() {
|
export function getMetadata() {
|
||||||
if (isStale) {
|
if (isStale) {
|
||||||
@@ -254,7 +227,7 @@ export function getMetadata() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommonParams = { rundown: OntimeRundown };
|
type CommonParams = { persistedRundown: OntimeRundown };
|
||||||
type MutationParams<T> = T & CommonParams;
|
type MutationParams<T> = T & CommonParams;
|
||||||
type MutatingReturn = {
|
type MutatingReturn = {
|
||||||
newRundown: OntimeRundown;
|
newRundown: OntimeRundown;
|
||||||
@@ -265,14 +238,23 @@ type MutatingFn<T extends object> = (params: MutationParams<T>) => MutatingRetur
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Decorators injects data into mutation
|
* Decorators injects data into mutation
|
||||||
* ensures order of operations when performing mutations
|
* @param mutation
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
||||||
function scopedMutation(params: T) {
|
async function scopedMutation(params: T) {
|
||||||
const { newEvent, newRundown, didMutate } = mutation({ ...params, rundown: persistedRundown });
|
/**
|
||||||
|
* Marking the data set as stale
|
||||||
|
* doing it before calling the mutation, gives the function a chance
|
||||||
|
* to prevent recalculation by setting stale = false
|
||||||
|
*/
|
||||||
|
isStale = true;
|
||||||
|
|
||||||
|
const { newEvent, newRundown, didMutate } = mutation({ ...params, persistedRundown });
|
||||||
|
|
||||||
// early return without calling side effects
|
// early return without calling side effects
|
||||||
if (!didMutate) {
|
if (!didMutate) {
|
||||||
|
isStale = false;
|
||||||
return { newEvent, newRundown, didMutate };
|
return { newEvent, newRundown, didMutate };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,34 +278,31 @@ export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AddArgs = MutationParams<{ atIndex: number; event: OntimeRundownEntry }>;
|
type AddArgs = MutationParams<{ atIndex: number; event: OntimeRundownEntry }>;
|
||||||
/**
|
|
||||||
* Add entry to rundown
|
export function add({ persistedRundown, atIndex, event }: AddArgs): Required<MutatingReturn> {
|
||||||
*/
|
|
||||||
export function add({ rundown, atIndex, event }: AddArgs): Required<MutatingReturn> {
|
|
||||||
const newEvent: OntimeRundownEntry = { ...event };
|
const newEvent: OntimeRundownEntry = { ...event };
|
||||||
const newRundown = insertAtIndex(atIndex, newEvent, rundown);
|
const newRundown = insertAtIndex(atIndex, newEvent, persistedRundown);
|
||||||
setIsStale();
|
|
||||||
return { newRundown, newEvent, didMutate: true };
|
return { newRundown, newEvent, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoveArgs = MutationParams<{ eventIds: string[] }>;
|
type RemoveArgs = MutationParams<{ eventIds: string[] }>;
|
||||||
/**
|
|
||||||
* Remove entry to rundown
|
export function remove({ persistedRundown, eventIds }: RemoveArgs): MutatingReturn {
|
||||||
*/
|
const newRundown = persistedRundown.filter((event) => !eventIds.includes(event.id));
|
||||||
export function remove({ rundown, eventIds }: RemoveArgs): MutatingReturn {
|
|
||||||
const newRundown = rundown.filter((event) => !eventIds.includes(event.id));
|
return { newRundown, didMutate: persistedRundown.length !== newRundown.length };
|
||||||
const didMutate = rundown.length !== newRundown.length;
|
|
||||||
if (didMutate) setIsStale();
|
|
||||||
return { newRundown, didMutate };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeAll(): MutatingReturn {
|
export function removeAll(): MutatingReturn {
|
||||||
setIsStale();
|
|
||||||
return { newRundown: [], didMutate: true };
|
return { newRundown: [], didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility function for patching an existing event with new data
|
* Utility function for patching events
|
||||||
|
* @param eventFromRundown
|
||||||
|
* @param patch
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
function makeEvent(eventFromRundown: OntimeRundownEntry, patch: Partial<OntimeRundownEntry>): OntimeRundownEntry {
|
function makeEvent(eventFromRundown: OntimeRundownEntry, patch: Partial<OntimeRundownEntry>): OntimeRundownEntry {
|
||||||
if (isOntimeEvent(eventFromRundown)) {
|
if (isOntimeEvent(eventFromRundown)) {
|
||||||
@@ -336,133 +315,120 @@ function makeEvent(eventFromRundown: OntimeRundownEntry, patch: Partial<OntimeRu
|
|||||||
}
|
}
|
||||||
|
|
||||||
type EditArgs = MutationParams<{ eventId: string; patch: Partial<OntimeRundownEntry> }>;
|
type EditArgs = MutationParams<{ eventId: string; patch: Partial<OntimeRundownEntry> }>;
|
||||||
/**
|
|
||||||
* Apply patch to an entry with given id
|
export function edit({ persistedRundown, eventId, patch }: EditArgs): Required<MutatingReturn> {
|
||||||
*/
|
const indexAt = persistedRundown.findIndex((event) => event.id === eventId);
|
||||||
export function edit({ rundown, eventId, patch }: EditArgs): Required<MutatingReturn> {
|
|
||||||
const indexAt = rundown.findIndex((event) => event.id === eventId);
|
|
||||||
if (indexAt < 0) {
|
if (indexAt < 0) {
|
||||||
throw new Error('Event not found');
|
throw new Error('Event not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patch?.type && rundown[indexAt].type !== patch.type) {
|
if (patch?.type && persistedRundown[indexAt].type !== patch.type) {
|
||||||
throw new Error('Invalid event type');
|
throw new Error('Invalid event type');
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventInMemory = rundown[indexAt];
|
const eventInMemory = persistedRundown[indexAt];
|
||||||
|
|
||||||
if (!hasChanges(eventInMemory, patch)) {
|
if (!hasChanges(eventInMemory, patch)) {
|
||||||
return { newRundown: rundown, newEvent: eventInMemory, didMutate: false };
|
isStale = false;
|
||||||
|
return { newRundown: persistedRundown, newEvent: eventInMemory, didMutate: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEvent = makeEvent(eventInMemory, patch);
|
const newEvent = makeEvent(eventInMemory, patch);
|
||||||
|
|
||||||
const newRundown = [...rundown];
|
const newRundown = [...persistedRundown];
|
||||||
newRundown[indexAt] = newEvent;
|
newRundown[indexAt] = newEvent;
|
||||||
|
|
||||||
// check whether the data warrants recalculation of cache
|
// check whether the data warrants recalculation of cache
|
||||||
const makeStale = isDataStale(patch);
|
const makeStale = isDataStale(patch);
|
||||||
|
|
||||||
if (makeStale) {
|
if (!makeStale) {
|
||||||
setIsStale();
|
rundown[newEvent.id] = newEvent;
|
||||||
} else {
|
|
||||||
normalisedRundown[newEvent.id] = newEvent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isStale = makeStale;
|
||||||
return { newRundown, newEvent, didMutate: true };
|
return { newRundown, newEvent, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
type BatchEditArgs = MutationParams<{ eventIds: string[]; patch: Partial<OntimeRundownEntry> }>;
|
type BatchEditArgs = MutationParams<{ eventIds: string[]; patch: Partial<OntimeRundownEntry> }>;
|
||||||
/**
|
|
||||||
* Apply patch to multiple entries
|
export function batchEdit({ persistedRundown, eventIds, patch }: BatchEditArgs): MutatingReturn {
|
||||||
*/
|
|
||||||
export function batchEdit({ rundown, eventIds, patch }: BatchEditArgs): MutatingReturn {
|
|
||||||
const ids = new Set(eventIds);
|
const ids = new Set(eventIds);
|
||||||
|
|
||||||
const newRundown = [];
|
const newRundown = [];
|
||||||
for (let i = 0; i < rundown.length; i++) {
|
for (let i = 0; i < persistedRundown.length; i++) {
|
||||||
if (ids.has(rundown[i].id)) {
|
if (ids.has(persistedRundown[i].id)) {
|
||||||
if (patch?.type && rundown[i].type !== patch.type) {
|
if (patch?.type && persistedRundown[i].type !== patch.type) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const newEvent = makeEvent(rundown[i], patch);
|
const newEvent = makeEvent(persistedRundown[i], patch);
|
||||||
newRundown.push(newEvent);
|
newRundown.push(newEvent);
|
||||||
} else {
|
} else {
|
||||||
newRundown.push(rundown[i]);
|
newRundown.push(persistedRundown[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsStale();
|
|
||||||
return { newRundown, didMutate: true };
|
return { newRundown, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReorderArgs = MutationParams<{ eventId: string; from: number; to: number }>;
|
type ReorderArgs = MutationParams<{ eventId: string; from: number; to: number }>;
|
||||||
/**
|
|
||||||
* Redorder two entries
|
export function reorder({ persistedRundown, eventId, from, to }: ReorderArgs): Required<MutatingReturn> {
|
||||||
*/
|
const event = persistedRundown[from];
|
||||||
export function reorder({ rundown, eventId, from, to }: ReorderArgs): Required<MutatingReturn> {
|
|
||||||
const event = rundown[from];
|
|
||||||
if (!event || eventId !== event.id) {
|
if (!event || eventId !== event.id) {
|
||||||
throw new Error('Event not found');
|
throw new Error('Event not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRundown = reorderArray(rundown, from, to);
|
const newRundown = reorderArray(persistedRundown, from, to);
|
||||||
for (let i = from; i <= to; i++) {
|
for (let i = from; i <= to; i++) {
|
||||||
const event = newRundown.at(i);
|
const event = newRundown.at(i);
|
||||||
if (isOntimeEvent(event)) {
|
if (isOntimeEvent(event)) {
|
||||||
event.revision += 1;
|
event.revision += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsStale();
|
|
||||||
return { newRundown, newEvent: newRundown.at(from) as OntimeRundownEntry, didMutate: true };
|
return { newRundown, newEvent: newRundown.at(from) as OntimeRundownEntry, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApplyDelayArgs = MutationParams<{ eventId: string }>;
|
type ApplyDelayArgs = MutationParams<{ eventId: string }>;
|
||||||
/**
|
|
||||||
* Apply a delay
|
export function applyDelay({ persistedRundown, eventId }: ApplyDelayArgs): MutatingReturn {
|
||||||
*/
|
const newRundown = apply(eventId, persistedRundown);
|
||||||
export function applyDelay({ rundown, eventId }: ApplyDelayArgs): MutatingReturn {
|
|
||||||
const newRundown = apply(eventId, rundown);
|
|
||||||
setIsStale();
|
|
||||||
return { newRundown, didMutate: true };
|
return { newRundown, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
type SwapArgs = MutationParams<{ fromId: string; toId: string }>;
|
type SwapArgs = MutationParams<{ fromId: string; toId: string }>;
|
||||||
/**
|
|
||||||
* Swap two entries
|
|
||||||
*/
|
|
||||||
export function swap({ rundown, fromId, toId }: SwapArgs): MutatingReturn {
|
|
||||||
const indexA = rundown.findIndex((event) => event.id === fromId);
|
|
||||||
const eventA = rundown.at(indexA);
|
|
||||||
|
|
||||||
const indexB = rundown.findIndex((event) => event.id === toId);
|
export function swap({ persistedRundown, fromId, toId }: SwapArgs): MutatingReturn {
|
||||||
const eventB = rundown.at(indexB);
|
const indexA = persistedRundown.findIndex((event) => event.id === fromId);
|
||||||
|
const eventA = persistedRundown.at(indexA);
|
||||||
|
|
||||||
|
const indexB = persistedRundown.findIndex((event) => event.id === toId);
|
||||||
|
const eventB = persistedRundown.at(indexB);
|
||||||
|
|
||||||
if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) {
|
if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) {
|
||||||
throw new Error('Swap only available for OntimeEvents');
|
throw new Error('Swap only available for OntimeEvents');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { newA, newB } = swapEventData(eventA, eventB);
|
const { newA, newB } = swapEventData(eventA, eventB);
|
||||||
const newRundown = [...rundown];
|
const newRundown = [...persistedRundown];
|
||||||
|
|
||||||
newRundown[indexA] = newA;
|
newRundown[indexA] = newA;
|
||||||
(newRundown[indexA] as OntimeEvent).revision += 1;
|
(newRundown[indexA] as OntimeEvent).revision += 1;
|
||||||
newRundown[indexB] = newB;
|
newRundown[indexB] = newB;
|
||||||
(newRundown[indexB] as OntimeEvent).revision += 1;
|
(newRundown[indexB] as OntimeEvent).revision += 1;
|
||||||
|
|
||||||
setIsStale();
|
|
||||||
return { newRundown, didMutate: true };
|
return { newRundown, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for invalidating service cache if a custom field is used
|
* Invalidates service cache if a custom field is used
|
||||||
|
* @param label
|
||||||
*/
|
*/
|
||||||
function invalidateIfUsed(label: CustomFieldLabel) {
|
function invalidateIfUsed(label: CustomFieldLabel) {
|
||||||
if (label in assignedCustomFields) {
|
if (label in assignedCustomFields) {
|
||||||
setIsStale();
|
isStale = true;
|
||||||
}
|
}
|
||||||
// if the field was in use, we mark the cache as stale
|
// if the field was in use, we mark the cache as stale
|
||||||
if (label in assignedCustomFields) {
|
if (label in assignedCustomFields) {
|
||||||
setIsStale();
|
isStale = true;
|
||||||
}
|
}
|
||||||
// ... and schedule a cache update
|
// ... and schedule a cache update
|
||||||
// schedule a non priority cache update
|
// schedule a non priority cache update
|
||||||
@@ -473,9 +439,10 @@ function invalidateIfUsed(label: CustomFieldLabel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for scheduling a non priority custom field persist
|
* Schedules a non priority custom field persist
|
||||||
|
* @param persistedCustomFields
|
||||||
*/
|
*/
|
||||||
function scheduleCustomFieldPersist() {
|
function scheduleCustomFieldPersist(persistedCustomFields: CustomFields) {
|
||||||
setImmediate(async () => {
|
setImmediate(async () => {
|
||||||
await getDataProvider().setCustomFields(persistedCustomFields);
|
await getDataProvider().setCustomFields(persistedCustomFields);
|
||||||
});
|
});
|
||||||
@@ -483,15 +450,12 @@ function scheduleCustomFieldPersist() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitises and creates a custom field in the database
|
* Sanitises and creates a custom field in the database
|
||||||
|
* @param field
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function createCustomField(field: CustomField): CustomFields {
|
export const createCustomField = async (field: CustomField) => {
|
||||||
const { label, type, colour } = field;
|
const { label, type, colour } = field;
|
||||||
const key = customFieldLabelToKey(label);
|
const key = customFieldLabelToKey(label);
|
||||||
|
|
||||||
if (key === null) {
|
|
||||||
throw new Error('Unable to convert label to a valid key');
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if label already exists
|
// check if label already exists
|
||||||
const alreadyExists = Object.hasOwn(persistedCustomFields, key);
|
const alreadyExists = Object.hasOwn(persistedCustomFields, key);
|
||||||
|
|
||||||
@@ -502,15 +466,18 @@ export function createCustomField(field: CustomField): CustomFields {
|
|||||||
// update object and persist
|
// update object and persist
|
||||||
persistedCustomFields[key] = { label, type, colour };
|
persistedCustomFields[key] = { label, type, colour };
|
||||||
|
|
||||||
scheduleCustomFieldPersist();
|
scheduleCustomFieldPersist(persistedCustomFields);
|
||||||
|
|
||||||
return persistedCustomFields;
|
return persistedCustomFields;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edits an existing custom field in the database
|
* Edits an existing custom field in the database
|
||||||
|
* @param key
|
||||||
|
* @param newField
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function editCustomField(key: string, newField: Partial<CustomField>): CustomFields {
|
export const editCustomField = async (key: string, newField: Partial<CustomField>) => {
|
||||||
if (!(key in persistedCustomFields)) {
|
if (!(key in persistedCustomFields)) {
|
||||||
throw new Error('Could not find label');
|
throw new Error('Could not find label');
|
||||||
}
|
}
|
||||||
@@ -520,14 +487,7 @@ export function editCustomField(key: string, newField: Partial<CustomField>): Cu
|
|||||||
throw new Error('Change of field type is not allowed');
|
throw new Error('Change of field type is not allowed');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newField.label === undefined) {
|
|
||||||
throw new Error('Missing label');
|
|
||||||
}
|
|
||||||
|
|
||||||
const newKey = customFieldLabelToKey(newField.label);
|
const newKey = customFieldLabelToKey(newField.label);
|
||||||
if (newKey === null) {
|
|
||||||
throw new Error('Unable to convert label to a valid key');
|
|
||||||
}
|
|
||||||
persistedCustomFields[newKey] = { ...existingField, ...newField };
|
persistedCustomFields[newKey] = { ...existingField, ...newField };
|
||||||
|
|
||||||
if (key !== newKey) {
|
if (key !== newKey) {
|
||||||
@@ -535,22 +495,23 @@ export function editCustomField(key: string, newField: Partial<CustomField>): Cu
|
|||||||
customFieldChangelog.set(key, newKey);
|
customFieldChangelog.set(key, newKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleCustomFieldPersist();
|
scheduleCustomFieldPersist(persistedCustomFields);
|
||||||
invalidateIfUsed(key);
|
invalidateIfUsed(key);
|
||||||
|
|
||||||
return persistedCustomFields;
|
return persistedCustomFields;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a custom field from the database
|
* Deletes a custom field from the database
|
||||||
|
* @param label
|
||||||
*/
|
*/
|
||||||
export function removeCustomField(label: string): CustomFields {
|
export const removeCustomField = async (label: string) => {
|
||||||
if (label in persistedCustomFields) {
|
if (label in persistedCustomFields) {
|
||||||
delete persistedCustomFields[label];
|
delete persistedCustomFields[label];
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleCustomFieldPersist();
|
scheduleCustomFieldPersist(persistedCustomFields);
|
||||||
invalidateIfUsed(label);
|
invalidateIfUsed(label);
|
||||||
|
|
||||||
return persistedCustomFields;
|
return persistedCustomFields;
|
||||||
}
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user