refactor: style cleanups and migrations

fix: close modal with button
refactor: small type improvements
refactor: migrate time inputs
refactor: migrate tooltips
refactor: prevent component resizing
This commit is contained in:
Carlos Valente
2025-07-06 21:33:46 +02:00
committed by Carlos Valente
parent 6facd6a666
commit 17a7d035bf
54 changed files with 521 additions and 469 deletions
+15 -12
View File
@@ -1,4 +1,5 @@
import { BrowserRouter } from 'react-router-dom';
import { Tooltip } from '@base-ui-components/react/tooltip';
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
@@ -20,20 +21,22 @@ function App() {
<ChakraProvider disableGlobalStyle resetCSS theme={theme}>
<QueryClientProvider client={ontimeQueryClient}>
<AppContextProvider>
<BrowserRouter basename={baseURI}>
<div className='App'>
<Tooltip.Provider>
<BrowserRouter basename={baseURI}>
<div className='App'>
<ErrorBoundary>
<TranslationProvider>
<IdentifyOverlay />
<AppRouter />
</TranslationProvider>
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</div>
<ErrorBoundary>
<TranslationProvider>
<IdentifyOverlay />
<AppRouter />
</TranslationProvider>
<div id='identify-portal' />
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</div>
<ErrorBoundary>
<div id='identify-portal' />
</ErrorBoundary>
</BrowserRouter>
</BrowserRouter>
</Tooltip.Provider>
</AppContextProvider>
</QueryClientProvider>
</ChakraProvider>
@@ -1,4 +1,4 @@
import { ButtonHTMLAttributes } from 'react';
import { ButtonHTMLAttributes, forwardRef } from 'react';
import { cx } from '../../utils/styleUtils';
@@ -17,20 +17,21 @@ interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
size?: 'small' | 'medium' | 'large' | 'xlarge';
}
export default function IconButton({
className,
children,
variant = 'subtle',
size = 'medium',
...buttonProps
}: IconButtonProps) {
return (
<button
className={cx([style.baseIconButton, style[variant], style[size], className])}
type='button'
{...buttonProps}
>
{children}
</button>
);
}
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
({ className, children, variant = 'subtle', size = 'medium', ...buttonProps }, ref) => {
return (
<button
ref={ref}
className={cx([style.baseIconButton, style[variant], style[size], className])}
type='button'
{...buttonProps}
>
{children}
</button>
);
},
);
IconButton.displayName = 'IconButton';
export default IconButton;
@@ -1,17 +0,0 @@
import { MouseEvent } from 'react';
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
interface TooltipActionBtnProps extends IconButtonProps {
clickHandler: (event: MouseEvent) => void | Promise<void>;
tooltip: string;
openDelay?: number;
}
export default function TooltipActionBtn(props: TooltipActionBtnProps) {
const { clickHandler, icon, size = 'xs', tooltip, openDelay = 0, className, ...rest } = props;
return (
<Tooltip label={tooltip} openDelay={openDelay}>
<IconButton {...rest} size={size} icon={icon} onClick={clickHandler} className={className} />
</Tooltip>
);
}
@@ -1,8 +1,7 @@
import { IoChevronDown, IoChevronUp } from 'react-icons/io5';
import { Tooltip } from '@chakra-ui/react';
import { tooltipDelayFast } from '../../../ontimeConfig';
import { millisToDelayString } from '../../utils/dateConfig';
import Tooltip from '../tooltip/Tooltip';
import style from './DelayIndicator.module.scss';
@@ -23,8 +22,8 @@ export default function DelayIndicator(props: DelayIndicatorProps) {
: millisToDelayString(delayValue);
return (
<Tooltip openDelay={tooltipDelayFast} label={delayString}>
<span className={style.delaySymbol}>{delayValue < 0 ? <IoChevronDown /> : <IoChevronUp />}</span>
<Tooltip text={delayString} render={<span />} className={style.delaySymbol}>
{delayValue < 0 ? <IoChevronDown /> : <IoChevronUp />}
</Tooltip>
);
}
@@ -18,8 +18,20 @@ interface NullableTimeInputProps<T extends string> {
className?: string;
}
export default function NullableTimeInput<T extends string>(props: NullableTimeInputProps<T>) {
const { id, name, submitHandler, time, emptyDisplay, placeholder, disabled, align = 'center', className } = props;
/**
* Similar to TimeInput, but allows clearing the time value
*/
export default function NullableTimeInput<T extends string>({
id,
name,
submitHandler,
time,
emptyDisplay,
placeholder,
disabled,
align = 'center',
className,
}: NullableTimeInputProps<T>) {
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState<string>('');
const ignoreChange = useRef(false);
@@ -3,4 +3,8 @@
max-width: 7.5em;
letter-spacing: 0.5px;
font-variant-numeric: tabular-nums;
&.delayed {
border: 1px solid $ontime-delay-text;
}
}
@@ -14,11 +14,21 @@ interface TimeInputProps<T extends string> {
placeholder?: string;
disabled?: boolean;
align?: 'left' | 'center';
delayed?: boolean;
className?: string;
}
export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
const { id, name, submitHandler, time, placeholder, disabled, align = 'center', className } = props;
export default function TimeInput<T extends string>({
id,
name,
submitHandler,
time,
placeholder,
disabled,
align = 'center',
delayed,
className,
}: TimeInputProps<T>) {
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState<string>('');
const ignoreChange = useRef(false);
@@ -122,7 +132,7 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
disabled={disabled}
ref={inputRef}
data-testid={`time-input-${name}`}
className={cx([style.timeInput, className])}
className={cx([style.timeInput, delayed && style.delayed, className])}
placeholder={placeholder}
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
@@ -130,7 +140,6 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
onKeyDown={onKeyDownHandler}
value={value}
maxLength={8}
autoComplete='off'
style={{
textAlign: align,
}}
@@ -1,10 +0,0 @@
$input-delayed-border-color: $ontime-delay-text;
.timeInput {
border: 1px solid transparent;
color: $label-gray;
&.delayed {
border: 1px solid $input-delayed-border-color;
}
}
@@ -1,37 +0,0 @@
import { PropsWithChildren } from 'react';
import { InputGroup } from '@chakra-ui/react';
import { cx } from '../../../utils/styleUtils';
import TimeInput from './TimeInput';
import style from './TimeInputWithButton.module.scss';
interface TimeInputWithButtonProps<T extends string> {
name: T;
submitHandler: (field: T, value: string) => void;
time?: number;
hasDelay?: boolean;
disabled?: boolean;
placeholder: string;
}
export default function TimeInputWithButton<T extends string>(props: PropsWithChildren<TimeInputWithButtonProps<T>>) {
const { name, submitHandler, time, hasDelay, placeholder, disabled, children } = props;
const inputClasses = cx([style.timeInput, hasDelay ? style.delayed : null]);
return (
<InputGroup size='sm' className={inputClasses} width='fit-content'>
<TimeInput<T>
name={name}
submitHandler={submitHandler}
time={time}
placeholder={placeholder}
align='left'
disabled={disabled}
/>
{children}
</InputGroup>
);
}
@@ -15,4 +15,9 @@
&:hover {
color: $ontime-color;
}
&:focus {
outline: none;
box-shadow: 0 1px 0 0 currentColor;
}
}
@@ -14,6 +14,7 @@
border-radius: 3px;
box-shadow: $box-shadow-l1;
border: 1px solid $gray-1100;
outline: none;
}
.backdrop {
@@ -0,0 +1,24 @@
.tooltip {
font-size: calc(1rem - 3px);
background-color: $ui-white;
color: $ui-black;
padding: 0.125rem 0.5rem;
border-radius: 2px;
line-height: 1.25em;
max-width: 200px;
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-instant] {
transition-duration: 0ms;
}
}
@@ -0,0 +1,24 @@
import { PropsWithChildren } from 'react';
import { Tooltip as BaseTooltip } from '@base-ui-components/react/tooltip';
import style from './Tooltip.module.scss';
interface TooltipProps extends BaseTooltip.Trigger.Props {
text: string;
}
export default function Tooltip({ text, children, ...triggerProps }: PropsWithChildren<TooltipProps>) {
return (
<BaseTooltip.Root>
<BaseTooltip.Trigger {...triggerProps}>{children}</BaseTooltip.Trigger>
<BaseTooltip.Portal>
<BaseTooltip.Positioner side='bottom' sideOffset={4}>
<BaseTooltip.Popup className={style.tooltip}>
<BaseTooltip.Arrow />
{text}
</BaseTooltip.Popup>
</BaseTooltip.Positioner>
</BaseTooltip.Portal>
</BaseTooltip.Root>
);
}
@@ -1,7 +1,7 @@
.corner {
position: absolute;
top: 1rem;
right: 2rem;
position: fixed;
top: 6rem;
right: 4rem;
z-index: $zindex-floating;
}
@@ -1,9 +1,8 @@
import { Fragment } from 'react';
import { Tooltip } from '@chakra-ui/react';
import Tooltip from '../../../common/components/tooltip/Tooltip';
import { isKeyEnter } from '../../../common/utils/keyEvent';
import { cx } from '../../../common/utils/styleUtils';
import { tooltipDelayFast } from '../../../ontimeConfig';
import { SettingsOption, SettingsOptionId, useAppSettingsMenu } from '../useAppSettingsMenu';
import useAppSettingsNavigation from '../useAppSettingsNavigation';
@@ -26,7 +25,7 @@ export default function PanelList({ selectedPanel, location }: PanelListProps) {
const isSelected = selectedPanel === panel.id;
if (panel.highlight) {
return (
<Tooltip key={panel.id} label={panel.highlight} openDelay={tooltipDelayFast} shouldWrapChildren>
<Tooltip key={panel.id} text={panel.highlight} render={<span />}>
<PanelListItem panel={panel} location={location} isSelected={isSelected} />
</Tooltip>
);
@@ -23,13 +23,13 @@ export default function FeaturePanel({ location }: PanelBaseProps) {
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Share Ontime Link</Panel.SubHeader>
<Panel.Divider />
{!isOntimeCloud && (
<>
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
<InfoNif />
</>
)}
<Panel.Divider />
<GenerateLinkFormExport />
</Panel.Card>
</Panel.Section>
@@ -73,18 +73,12 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, isLockedToV
};
return (
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)} onKeyDown={(event) => preventEscape(event)}>
<form onSubmit={handleSubmit(onSubmit)} onKeyDown={(event) => preventEscape(event)}>
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
{!isLockedToView ? (
<Info>
<Panel.Paragraph>
You can generate a link to share with your team or to use in automation (such as companion).
</Panel.Paragraph>
</Info>
<Info>You can generate a link to share with your team or to use in automation (such as companion).</Info>
) : (
<Info>
<Panel.Paragraph>You can generate a link to share with your team</Panel.Paragraph>
</Info>
<Info>You can generate a link to share with your team</Info>
)}
<Panel.ListGroup>
<Panel.ListItem>
@@ -136,6 +130,6 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, isLockedToV
</div>
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Section>
</form>
);
}
@@ -8,10 +8,10 @@ import { postUrlPresets } from '../../../../common/api/urlPresets';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import IconButton from '../../../../common/components/buttons/IconButton';
import TooltipActionBtn from '../../../../common/components/buttons/TooltipActionBtn';
import Info from '../../../../common/components/info/Info';
import Input from '../../../../common/components/input/input/Input';
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
import Tooltip from '../../../../common/components/tooltip/Tooltip';
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
import { preventEscape } from '../../../../common/utils/keyEvent';
import { handleLinks } from '../../../../common/utils/linkUtils';
@@ -194,17 +194,15 @@ export default function UrlPresetsForm() {
<Panel.Error>{maybeUrlError}</Panel.Error>
</td>
<Panel.InlineElements relation='inner' as='td'>
<TooltipActionBtn
size='sm'
isDisabled={!canTest}
clickHandler={(event) => handleLinks(preset.alias, event)}
tooltip='Test preset'
aria-label='Test preset'
variant='ontime-ghosted'
color='#e2e2e2' // $gray-200
icon={<IoOpenOutline />}
<Tooltip
text='Test preset'
render={<IconButton variant='ghosted-white' />}
data-testid={`field__test_${index}`}
/>
onClick={(event) => handleLinks(preset.alias, event)}
disabled={!canTest}
>
<IoOpenOutline />
</Tooltip>
<IconButton
onClick={() => remove(index)}
variant='ghosted-destructive'
@@ -1,12 +1,13 @@
import { useEffect, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { IoAdd, IoTrash } from 'react-icons/io5';
import { Select, Tooltip } from '@chakra-ui/react';
import { Select } from '@chakra-ui/react';
import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils';
import Button from '../../../../../../common/components/buttons/Button';
import IconButton from '../../../../../../common/components/buttons/IconButton';
import Input from '../../../../../../common/components/input/input/Input';
import Tooltip from '../../../../../../common/components/tooltip/Tooltip';
import * as Panel from '../../../../panel-utils/PanelUtils';
import useGoogleSheet from '../useGoogleSheet';
import { useSheetStore } from '../useSheetStore';
@@ -97,10 +98,13 @@ export default function ImportMapForm(props: ImportMapFormProps) {
Import options
<Panel.InlineElements>
{!isSpreadsheet && (
<Tooltip label='Revoke the google authentication'>
<Button onClick={handleRevoke} disabled={isLoading}>
Revoke
</Button>
<Tooltip
text='Revoke the google authentication'
render={<Button />}
onClick={handleRevoke}
disabled={isLoading}
>
Revoke
</Tooltip>
)}
<Button onClick={onCancel} disabled={isLoading}>
@@ -1,13 +1,12 @@
import { IoArrowDown, IoArrowUp, IoBan, IoFlag, IoTime } from 'react-icons/io5';
import { Tooltip } from '@chakra-ui/react';
import { TimerPhase, TimerType } from 'ontime-types';
import { Corner } from '../../../common/components/editor-utils/EditorUtils';
import Tooltip from '../../../common/components/tooltip/Tooltip';
import { useMessagePreview } from '../../../common/hooks/useSocket';
import useViewSettings from '../../../common/hooks-query/useViewSettings';
import { handleLinks } from '../../../common/utils/linkUtils';
import { cx, timerPlaceholder } from '../../../common/utils/styleUtils';
import { tooltipDelayMid } from '../../../ontimeConfig';
import style from './MessageControl.module.scss';
@@ -64,20 +63,45 @@ export default function TimerPreview() {
{secondary !== null && <div className={style.secondaryContent}>{secondary}</div>}
</div>
<div className={style.eventStatus}>
<Tooltip label='Time type: Count down' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoArrowDown className={style.statusIcon} data-active={timerType === TimerType.CountDown} />
<Tooltip
text='Time type: Count down'
render={<span />}
className={style.statusIcon}
data-active={timerType === TimerType.CountDown}
>
<IoArrowDown />
</Tooltip>
<Tooltip label='Time type: Count up' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoArrowUp className={style.statusIcon} data-active={timerType === TimerType.CountUp} />
<Tooltip
text='Time type: Count up'
render={<span />}
className={style.statusIcon}
data-active={timerType === TimerType.CountUp}
>
<IoArrowUp />
</Tooltip>
<Tooltip label='Time type: Clock' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoTime className={style.statusIcon} data-active={timerType === TimerType.Clock} />
<Tooltip
text='Time type: Clock'
render={<span />}
className={style.statusIcon}
data-active={timerType === TimerType.Clock}
>
<IoTime />
</Tooltip>
<Tooltip label='Time type: None' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoBan className={style.statusIcon} data-active={timerType === TimerType.None} />
<Tooltip
text='Time type: None'
render={<span />}
className={style.statusIcon}
data-active={timerType === TimerType.None}
>
<IoBan />
</Tooltip>
<Tooltip label={countToEnd ? 'Count to end' : 'Count duration'} openDelay={tooltipDelayMid} shouldWrapChildren>
<IoFlag className={style.statusIcon} data-active={countToEnd} />
<Tooltip
text={countToEnd ? 'Count to end' : 'Count duration'}
render={<span />}
className={style.statusIcon}
data-active={countToEnd}
>
<IoFlag />
</Tooltip>
</div>
</div>
@@ -4,6 +4,7 @@
}
.auxTimers {
display: flex;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 0.5rem;
}
@@ -1,12 +1,10 @@
import { IoAdd, IoRemove } from 'react-icons/io5';
import { Tooltip } from '@chakra-ui/react';
import { useLocalStorage } from '@mantine/hooks';
import { Playback } from 'ontime-types';
import { MILLIS_PER_HOUR, parseUserTime } from 'ontime-utils';
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import { setPlayback } from '../../../../common/hooks/useSocket';
import { tooltipDelayMid } from '../../../../ontimeConfig';
import TapButton from '../tap-button/TapButton';
import style from './AddTime.module.scss';
@@ -41,16 +39,12 @@ export default function AddTime(props: AddTimeProps) {
<div className={style.addTime}>
<TimeInput name='addtime' submitHandler={handleTimeChange} time={timeInMs} placeholder='Add time' />
<div className={style.addButtons}>
<Tooltip label='Remove time' openDelay={tooltipDelayMid} shouldWrapChildren>
<TapButton onClick={() => handleAddTime('remove')} disabled={doDisableButtons} className={style.tallButtons}>
<IoRemove />
</TapButton>
</Tooltip>
<Tooltip label='Add time' openDelay={tooltipDelayMid} shouldWrapChildren>
<TapButton onClick={() => handleAddTime('add')} disabled={doDisableButtons} className={style.tallButtons}>
<IoAdd />
</TapButton>
</Tooltip>
<TapButton onClick={() => handleAddTime('remove')} disabled={doDisableButtons} className={style.tallButtons}>
<IoRemove />
</TapButton>
<TapButton onClick={() => handleAddTime('add')} disabled={doDisableButtons} className={style.tallButtons}>
<IoAdd />
</TapButton>
</div>
</div>
);
@@ -22,3 +22,24 @@
gap: 0.25rem;
height: 1.5rem;
}
.fakeInput {
box-sizing: border-box;
width: 100%;
max-width: 7.5em;
height: 2rem;
display: grid;
place-content: center;
font-size: 1rem;
font-weight: 400;
color: $gray-200;
border-radius: $component-border-radius-md;
border: 1px solid transparent;
letter-spacing: 0.5px;
font-variant-numeric: tabular-nums;
padding-inline: 0.5em;
outline: none;
}
@@ -1,6 +1,6 @@
import { IoArrowDown, IoArrowUp, IoPause, IoPlay, IoStop } from 'react-icons/io5';
import { Playback, SimpleDirection, SimplePlayback } from 'ontime-types';
import { parseUserTime } from 'ontime-utils';
import { millisToString, parseUserTime } from 'ontime-utils';
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import { setAuxTimer, useAuxTimerControl, useAuxTimerTime } from '../../../../common/hooks/useSocket';
@@ -22,7 +22,7 @@ export function AuxTimer({ index }: AuxTimerProps) {
setDirection(index, newDirection);
};
const canStop = playback !== SimplePlayback.Stop;
const isActive = playback !== SimplePlayback.Stop;
const playbackAction = playback === SimplePlayback.Start ? 'pause' : 'play';
return (
@@ -30,15 +30,15 @@ export function AuxTimer({ index }: AuxTimerProps) {
Aux Timer {index}
<div className={style.controls}>
<div className={style.input}>
<AuxTimerInput index={index} />
<TapButton onClick={toggleDirection} aspect='tight'>
<AuxTimerInput index={index} isActive={isActive} />
<TapButton onClick={toggleDirection} aspect='tight' disabled={isActive}>
{direction === SimpleDirection.CountDown && <IoArrowDown data-testid={`aux-timer-direction-${index}`} />}
{direction === SimpleDirection.CountUp && <IoArrowUp data-testid={`aux-timer-direction-${index}`} />}
</TapButton>
</div>
<div className={style.twoSides}>
<AuxTogglePlay index={index} action={playbackAction} />
<TapButton onClick={() => stop(index)} theme={Playback.Stop} disabled={!canStop}>
<TapButton onClick={() => stop(index)} theme={Playback.Stop} disabled={!isActive}>
<IoStop data-testid={`aux-timer-stop-${index}`} />
</TapButton>
</div>
@@ -47,11 +47,12 @@ export function AuxTimer({ index }: AuxTimerProps) {
);
}
interface AuxTimerInput {
interface AuxTimerInputProps {
index: number;
isActive: boolean;
}
function AuxTimerInput({ index }: AuxTimerProps) {
function AuxTimerInput({ index, isActive }: AuxTimerInputProps) {
const newTimeInMs = useAuxTimerTime(index);
const { setDuration } = setAuxTimer;
@@ -60,6 +61,14 @@ function AuxTimerInput({ index }: AuxTimerProps) {
setDuration(index, newTimeInMs);
};
if (isActive) {
return (
<div className={style.fakeInput} data-testid={`time-label-aux${index}`}>
{millisToString(newTimeInMs)}
</div>
);
}
return (
<TimeInput submitHandler={handleTimeUpdate} name={`aux${index}`} time={newTimeInMs} placeholder={`Aux ${index}`} />
);
@@ -1,11 +1,9 @@
import { useMemo } from 'react';
import { IoPause, IoPlay, IoPlaySkipBack, IoPlaySkipForward, IoReload, IoStop, IoTime } from 'react-icons/io5';
import { Tooltip } from '@chakra-ui/react';
import { Playback, TimerPhase } from 'ontime-types';
import { validatePlayback } from 'ontime-utils';
import { setPlayback } from '../../../../common/hooks/useSocket';
import { tooltipDelayMid } from '../../../../ontimeConfig';
import TapButton from '../tap-button/TapButton';
import style from './PlaybackButtons.module.scss';
@@ -66,31 +64,23 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
</TapButton>
</div>
<div className={style.transportContainer}>
<Tooltip label='Previous event' openDelay={tooltipDelayMid}>
<TapButton onClick={setPlayback.previous} disabled={disablePrev}>
<IoPlaySkipBack />
</TapButton>
</Tooltip>
<Tooltip label='Next event' openDelay={tooltipDelayMid}>
<TapButton onClick={setPlayback.next} disabled={disableNext}>
<IoPlaySkipForward />
</TapButton>
</Tooltip>
<TapButton onClick={setPlayback.previous} disabled={disablePrev}>
<IoPlaySkipBack />
</TapButton>
<TapButton onClick={setPlayback.next} disabled={disableNext}>
<IoPlaySkipForward />
</TapButton>
</div>
<div className={style.extra}>
<TapButton onClick={setPlayback.roll} disabled={disableRoll} theme={Playback.Roll} active={isRolling}>
<IoTime />
</TapButton>
<Tooltip label='Reload event' openDelay={tooltipDelayMid}>
<TapButton onClick={setPlayback.reload} disabled={disableReload}>
<IoReload className={style.invertX} />
</TapButton>
</Tooltip>
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
<TapButton onClick={setPlayback.stop} disabled={disableStop} theme={Playback.Stop}>
<IoStop />
</TapButton>
</Tooltip>
<TapButton onClick={setPlayback.reload} disabled={disableReload}>
<IoReload className={style.invertX} />
</TapButton>
<TapButton onClick={setPlayback.stop} disabled={disableStop} theme={Playback.Stop}>
<IoStop />
</TapButton>
</div>
</div>
);
@@ -1,9 +1,9 @@
import { PropsWithChildren } from 'react';
import { Tooltip } from '@chakra-ui/react';
import { MaybeNumber, Playback, TimerPhase } from 'ontime-types';
import { dayInMs, millisToString } from 'ontime-utils';
import AppLink from '../../../../common/components/link/app-link/AppLink';
import Tooltip from '../../../../common/components/tooltip/Tooltip';
import { useTimer } from '../../../../common/hooks/useSocket';
import useReport from '../../../../common/hooks-query/useReport';
import { formatDuration } from '../../../../common/utils/time';
@@ -43,13 +43,9 @@ export default function PlaybackTimer(props: PropsWithChildren<PlaybackTimerProp
return (
<div className={style.timeContainer}>
<div className={style.indicators}>
<Tooltip label={rollLabel}>
<div className={style.indicatorRoll} data-active={isRolling} />
</Tooltip>
<Tooltip text={rollLabel} render={<div />} className={style.indicatorRoll} data-active={isRolling} />
<div className={style.indicatorNegative} data-active={isOvertime} />
<Tooltip label={addedTimeLabel}>
<div className={style.indicatorDelay} data-active={hasAddedTime} />
</Tooltip>
<Tooltip text={addedTimeLabel} render={<div />} className={style.indicatorDelay} data-active={hasAddedTime} />
</div>
<TimerDisplay time={isWaiting ? timer.secondaryTimer : timer.current} />
<div className={style.status}>
@@ -1,5 +1,4 @@
import { Tooltip } from '@chakra-ui/react';
import Tooltip from '../../../common/components/tooltip/Tooltip';
import { cx } from '../../../common/utils/styleUtils';
import style from './TimeLayout.module.scss';
@@ -29,8 +28,12 @@ export function TimeRow({ label, value, daySpan, muted, className }: TimeLayoutP
<div className={style.row}>
<span className={style.label}>{label}</span>
{daySpan ? (
<Tooltip label={`Event spans over ${daySpan + 1} days`}>
<span className={cx([style.clock, style.daySpan, className])}>{value}</span>
<Tooltip
text={`Event spans over ${daySpan + 1} days`}
render={<span />}
className={cx([style.clock, style.daySpan, className])}
>
{value}
</Tooltip>
) : (
<span className={cx([style.clock, muted && style.muted, className])}>{value}</span>
@@ -22,6 +22,7 @@ $block-cursor-color: $orange-400;
border-radius: $block-border-radius;
position: relative;
color: $block-text-color;
overflow: hidden;
min-width: $block-width;
}
@@ -1,6 +1,5 @@
import { memo } from 'react';
import { IoInformationCircle } from 'react-icons/io5';
import { Tooltip } from '@chakra-ui/react';
import { EndAction, TimerType, TimeStrategy } from 'ontime-types';
import { millisToString, parseUserTime } from 'ontime-utils';
@@ -8,6 +7,7 @@ import * as Editor from '../../../../common/components/editor-utils/EditorUtils'
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import Select from '../../../../common/components/select/Select';
import Switch from '../../../../common/components/switch/Switch';
import Tooltip from '../../../../common/components/tooltip/Tooltip';
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
import { millisToDelayString } from '../../../../common/utils/dateConfig';
import TimeInputFlow from '../../time-input-flow/TimeInputFlow';
@@ -126,11 +126,12 @@ function EventEditorTimes({
<div className={style.column}>
<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
text='Changes how the timer is displayed in different views. It is not reflected in the rundown'
render={<span />}
>
Display Options
<IoInformationCircle className={style.tooltipIcon} />
</Tooltip>
</Editor.Title>
<div className={style.splitTwo}>
@@ -1,6 +1,5 @@
import { Fragment, useCallback, useMemo, useState } from 'react';
import { IoAlertCircle, IoCheckmarkCircle, IoTrash } from 'react-icons/io5';
import { Tooltip } from '@chakra-ui/react';
import { TimerLifeCycle, timerLifecycleValues, Trigger } from 'ontime-types';
import { generateId } from 'ontime-utils';
@@ -8,6 +7,7 @@ import Button from '../../../../common/components/buttons/Button';
import IconButton from '../../../../common/components/buttons/IconButton';
import Select from '../../../../common/components/select/Select';
import Tag from '../../../../common/components/tag/Tag';
import Tooltip from '../../../../common/components/tooltip/Tooltip';
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
@@ -107,7 +107,7 @@ function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) {
Add
</Button>
{validationError !== undefined ? (
<Tooltip label={validationError} shouldWrapChildren>
<Tooltip text={validationError} render={<span />}>
<IoAlertCircle className={style.errorLabel} />
</Tooltip>
) : (
@@ -2,21 +2,21 @@
.block {
@include block-styling;
overflow: hidden;
margin-block: 1rem 0.25rem;
display: grid;
grid-template-columns: 2rem 1fr;
grid-template-areas: 'binder header';
align-items: center;
// TODO(style fix): groups have an extra bottom margin which interrupt colour
margin-block: 0.25rem;
&.hasCursor {
outline: 1px solid $block-cursor-color;
}
&.expanded {
margin-block: 1rem 0;
border-radius: $block-border-radius $block-border-radius 0 0;
border-bottom: 0.25rem solid color-mix(in srgb, transparent 90%, var(--user-bg, transparent) 10%);
}
.binder {
@@ -29,8 +29,12 @@
place-content: center;
position: relative;
cursor: pointer;
}
&:focus {
outline: 1px solid $blue-500;
outline-offset: -1px;
}
}
.header {
grid-area: header;
@@ -102,8 +102,9 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
onClick={handleFocusClick}
onContextMenu={onContextMenu}
style={{
...(binderColours ? { '--user-bg': binderColours.backgroundColor } : {}),
// ...(binderColours ? { '--user-bg': binderColours.backgroundColor } : {}),
...dragStyle,
'--user-bg': data.colour || '#929292',
}}
data-testid='rundown-block'
>
@@ -4,12 +4,11 @@
@include block-styling;
margin-block: 0.25rem;
padding-right: 0.25rem;
background-color: $block-bg2;
padding-right: 0.5rem;
display: grid;
grid-template-columns: 2rem 1fr auto;
grid-template-areas: 'drag inpt btns';
grid-template-columns: 2rem 1fr auto auto;
align-items: center;
height: $secondary-block-height;
gap: 0.5rem;
@@ -21,12 +20,4 @@
.drag {
@include drag-style;
grid-area: drag;
}
.actionButtons {
grid-area: btns;
display: flex;
align-items: center;
gap: 0.5rem;
}
@@ -55,23 +55,24 @@ export default function RundownDelay({ data, hasCursor }: RundownDelayProps) {
deleteEntry([data.id]);
};
const blockClasses = cx([style.delay, hasCursor ? style.hasCursor : null]);
return (
<div className={blockClasses} ref={setNodeRef} style={dragStyle} data-testid='rundown-delay'>
<div
className={cx([style.delay, hasCursor ? style.hasCursor : null])}
ref={setNodeRef}
style={dragStyle}
data-testid='rundown-delay'
>
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
<IoReorderTwo />
</span>
<DelayInput eventId={data.id} duration={data.duration} />
<div className={style.actionButtons}>
<Button onClick={applyDelayHandler} variant='ghosted-white'>
<IoCheckmarkDone /> Make permanent
</Button>
<Button onClick={cancelDelayHandler} variant='ghosted-white'>
<IoClose />
Cancel
</Button>
</div>
<Button onClick={applyDelayHandler} variant='ghosted-white'>
<IoCheckmarkDone /> Make permanent
</Button>
<Button onClick={cancelDelayHandler} variant='ghosted-white'>
<IoClose />
Cancel
</Button>
</div>
);
}
@@ -6,6 +6,7 @@ $skip-opacity: 0.2;
@include block-styling;
background-color: $block-bg;
margin-block: 0.25rem;
overflow: initial;
display: grid;
grid-template-areas:
@@ -15,7 +16,7 @@ $skip-opacity: 0.2;
'binder pb-actions estatus estatus'
'binder ... ... ...';
grid-template-columns: $block-binder-width 3rem 1fr auto;
grid-template-columns: $block-binder-width 3rem 1fr 3rem;
grid-template-rows: 0.125rem 2rem 2rem auto 0.125rem;
align-items: center;
padding-right: $block-clearance;
@@ -92,11 +93,10 @@ $skip-opacity: 0.2;
position: relative;
cursor: pointer;
border-radius: $block-border-radius 0 0 $block-border-radius;
background-color: $gray-1050; // to override inline
color: $section-white;
font-size: 1rem;
border-radius: 3px 0 0 3px;
.drag {
@include drag-style;
@@ -10,11 +10,10 @@ import {
IoPlaySkipForward,
IoTime,
} from 'react-icons/io5';
import { Tooltip } from '@chakra-ui/react';
import { EndAction, Playback, TimerType, TimeStrategy } from 'ontime-types';
import Tooltip from '../../../common/components/tooltip/Tooltip';
import { cx } from '../../../common/utils/styleUtils';
import { tooltipDelayMid } from '../../../ontimeConfig';
import EditableBlockTitle from '../common/EditableBlockTitle';
import TimeInputFlow from '../time-input-flow/TimeInputFlow';
@@ -137,25 +136,17 @@ function RundownEventInner({
{loaded && <EventBlockProgressBar />}
</div>
<div className={style.eventStatus} tabIndex={-1}>
<Tooltip label={`Time type: ${timerType}`} openDelay={tooltipDelayMid}>
<span>
<TimerIcon type={timerType} className={style.statusIcon} />
</span>
<Tooltip text={`Time type: ${timerType}`} render={<span />}>
<TimerIcon type={timerType} className={style.statusIcon} />
</Tooltip>
<Tooltip label={`End action: ${endAction}`} openDelay={tooltipDelayMid}>
<span>
<EndActionIcon action={endAction} className={style.statusIcon} />
</span>
<Tooltip text={`End action: ${endAction}`} render={<span />}>
<EndActionIcon action={endAction} className={style.statusIcon} />
</Tooltip>
<Tooltip label={`${countToEnd ? 'Count to End' : 'Count duration'}`} openDelay={tooltipDelayMid}>
<span>
<IoFlag className={`${style.statusIcon} ${countToEnd ? style.active : style.disabled}`} />
</span>
<Tooltip text={`${countToEnd ? 'Count to End' : 'Count duration'}`} render={<span />}>
<IoFlag className={`${style.statusIcon} ${countToEnd ? style.active : style.disabled}`} />
</Tooltip>
<Tooltip label='Event has Triggers' openDelay={tooltipDelayMid}>
<span>
<IoFlash className={`${style.statusIcon} ${hasTriggers ? style.active : style.disabled}`} />
</span>
<Tooltip text='Event has Triggers' render={<span />}>
<IoFlash className={`${style.statusIcon} ${hasTriggers ? style.active : style.disabled}`} />
</Tooltip>
</div>
</div>
@@ -5,7 +5,7 @@ $gap-left: calc(2rem + 0.25rem + 3rem);
font-size: calc(1rem - 5px);
position: absolute;
top: -1em;
z-index: $zindex-floating;;
z-index: $zindex-floating;
margin-left: $gap-left;
display: flex;
@@ -1,11 +1,9 @@
.chip {
background-color: $gray-1100;
white-space: nowrap;
font-size: calc(1rem - 3px);
color: $label-gray;
padding: 0.125rem 0.5rem;
border-radius: 2px;
justify-self: end;
&.over {
color: $ontime-delay-text;
@@ -1,13 +1,12 @@
import { useMemo } from 'react';
import { IoCheckmarkCircle } from 'react-icons/io5';
import { Tooltip } from '@chakra-ui/react';
import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
import Tooltip from '../../../../common/components/tooltip/Tooltip';
import { usePlayback } from '../../../../common/hooks/useSocket';
import useReport from '../../../../common/hooks-query/useReport';
import { cx } from '../../../../common/utils/styleUtils';
import { formatDuration, formatTime, useTimeUntilStart } from '../../../../common/utils/time';
import { tooltipDelayFast } from '../../../../ontimeConfig';
import style from './RundownEventChip.module.scss';
@@ -39,7 +38,7 @@ export default function RundownEventChip({
const { playback } = usePlayback();
if (isLoaded) {
return null; //TODO: the is a small flash of 'DUE' on the loaded event as clock data arrives before isLoaded propagates
return null;
}
const playbackActive = isPlaybackActive(playback);
@@ -51,16 +50,14 @@ export default function RundownEventChip({
if (playbackActive) {
// we extracted the component to avoid unnecessary calculations and re-renders
return (
<Tooltip label='Expected time until start' openDelay={tooltipDelayFast}>
<div className={className}>
<EventUntil
timeStart={timeStart}
delay={delay}
dayOffset={dayOffset}
totalGap={totalGap}
isLinkedToLoaded={isLinkedToLoaded}
/>
</div>
<Tooltip text='Expected time until start' render={<span />} className={className}>
<EventUntil
timeStart={timeStart}
delay={delay}
dayOffset={dayOffset}
totalGap={totalGap}
isLinkedToLoaded={isLinkedToLoaded}
/>
</Tooltip>
);
}
@@ -113,7 +110,7 @@ function EventReport(props: EventReportProps) {
const absDifference = Math.abs(difference);
if (absDifference < MILLIS_PER_SECOND) {
return ['ontime', 'ontime', 'Event finished ontime'];
return ['ontime', 'under', 'Event finished on time'];
}
const isOver = difference > 0;
@@ -131,10 +128,8 @@ function EventReport(props: EventReportProps) {
}
return (
<Tooltip label={tooltip} openDelay={tooltipDelayFast}>
<div className={cx([style.chip, style[overUnderStyle], className])}>
{value === 'ontime' ? <IoCheckmarkCircle size='1.1rem' /> : value}
</div>
<Tooltip text={tooltip} render={<span />} className={cx([style.chip, style[overUnderStyle], className])}>
{value === 'ontime' ? <IoCheckmarkCircle size='1.1rem' /> : value}
</Tooltip>
);
}
@@ -1,28 +1,13 @@
import { memo, MouseEvent } from 'react';
import { IoPause, IoPlay, IoReload, IoRemoveCircle, IoRemoveCircleOutline } from 'react-icons/io5';
import TooltipActionBtn from '../../../../common/components/buttons/TooltipActionBtn';
import IconButton from '../../../../common/components/buttons/IconButton';
import Tooltip from '../../../../common/components/tooltip/Tooltip';
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
import { setEventPlayback } from '../../../../common/hooks/useSocket';
import { tooltipDelayMid } from '../../../../ontimeConfig';
import style from '../RundownEvent.module.scss';
const blockBtnStyle = {
size: 'sm',
};
type StyleVariant = {
'aria-label': string;
tooltip: string;
backgroundColor: string;
_hover: { backgroundColor?: string };
};
const tooltipProps = {
openDelay: tooltipDelayMid,
};
interface RundownEventPlaybackProps {
eventId: string;
skip: boolean;
@@ -67,67 +52,66 @@ function RundownEventPlayback({
setEventPlayback.loadEvent(eventId);
};
const buttonVariant: Partial<StyleVariant> = {};
if (isPaused) {
// continue
buttonVariant['aria-label'] = 'Continue event';
buttonVariant.tooltip = 'Continue event';
buttonVariant.backgroundColor = '#339E4E';
buttonVariant._hover = { backgroundColor: '#339E4Eee' };
} else if (isPlaying) {
// pause
buttonVariant['aria-label'] = 'Pause event';
buttonVariant.tooltip = 'Pause event';
buttonVariant.backgroundColor = '#c05621';
buttonVariant._hover = { backgroundColor: '#c05621ee' };
} else {
// start
buttonVariant['aria-label'] = 'Start event';
buttonVariant.tooltip = 'Start event';
if (!disablePlayback) {
buttonVariant._hover = { backgroundColor: '#339E4E' };
const playButtonStyles: { tooltip: string; backgroundColor: string | undefined } = (() => {
if (isPaused) {
return {
tooltip: 'Continue event',
backgroundColor: '#339E4E',
};
}
}
if (isPlaying) {
return {
tooltip: 'Pause event',
backgroundColor: '#c05621',
};
}
return {
tooltip: 'Start event',
backgroundColor: undefined,
};
})();
return (
<div className={style.playbackActions}>
<TooltipActionBtn
variant='ontime-subtle-white'
<Tooltip
text='Skip event'
render={<IconButton variant='subtle-white' />}
onClick={toggleSkip}
tabIndex={-1}
disabled={loaded}
style={{
background: skip ? '#9A0000' : undefined,
}}
aria-label='Skip event'
tooltip='Skip event'
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
backgroundColor={skip ? '#B20000' : undefined}
_hover={{ backgroundColor: '#FF7878' }}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={toggleSkip}
>
{skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
</Tooltip>
<Tooltip
text='Load event'
render={<IconButton variant='subtle-white' />}
onClick={load}
tabIndex={-1}
isDisabled={loaded}
/>
<TooltipActionBtn
variant='ontime-subtle-white'
disabled={disablePlayback}
aria-label='Load event'
tooltip='Load event'
icon={<IoReload className={style.flip} />}
isDisabled={disablePlayback}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={load}
>
<IoReload className={style.flip} />
</Tooltip>
<Tooltip
text={playButtonStyles.tooltip}
render={<IconButton variant='subtle-white' />}
onClick={actionHandler}
tabIndex={-1}
/>
<TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Start event'
tooltip='Start event'
icon={!isPlaying ? <IoPlay /> : <IoPause />}
isDisabled={disablePlayback}
{...tooltipProps}
{...blockBtnStyle}
{...buttonVariant}
clickHandler={actionHandler}
tabIndex={-1}
/>
disabled={disablePlayback}
style={{
backgroundColor: playButtonStyles.backgroundColor,
}}
aria-label={isPlaying ? 'Pause event' : 'Start event'}
>
{!isPlaying ? <IoPlay /> : <IoPause />}
</Tooltip>
</div>
);
}
@@ -1,30 +1,63 @@
.timeLabel {
position: absolute;
z-index: $zindex-floating;
font-size: 0.75rem;
font-weight: 600;
top: 2px;
right: 4px;
color: inherit;
}
.timeAction {
opacity: 0.4;
background: $gray-1050;
color: $gray-500;
cursor: pointer;
padding-right: 0.5em;
height: 2rem;
width: 2rem;
display: grid;
place-content: center;
aspect-ratio: 1;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
}
&:active:not(:disabled) {
background: $gray-1300;
}
&:focus {
outline: 1px solid $blue-500;
outline-offset: -1px;
}
&.active {
opacity: 1;
color: var(--status-color-active-override, $active-indicator);
}
.fourtyfive {
transform: rotate(-45deg);
}
}
.fourtyfive {
transform: rotate(-45deg);
}
.timerNote {
width: 1.25em;
color: $blue-500;
margin-right: 0.5rem;
font-size: 1.5em;
display: grid;
}
.inputGroup {
border: 1px solid transparent;
width: fit-content;
display: flex;
align-items: center;
border-radius: $component-border-radius-md;
&.delayed {
border: 1px solid $ontime-delay-text;
}
input {
max-width: 6.5em;
border-radius: $component-border-radius-md 0 0 $component-border-radius-md;
border: none;
padding-right: 0;
}
button {
width: 1.5rem;
height: 2rem;
border-radius: 0 $component-border-radius-md $component-border-radius-md 0;
border: none;
}
}
@@ -1,14 +1,13 @@
import { memo } from 'react';
import { IoAlertCircleOutline, IoLink, IoLockClosed, IoLockOpenOutline, IoUnlink } from 'react-icons/io5';
import { InputRightElement, Tooltip } from '@chakra-ui/react';
import { TimeField, TimeStrategy } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
import TimeInputWithButton from '../../../common/components/input/time-input/TimeInputWithButton';
import TimeInput from '../../../common/components/input/time-input/TimeInput';
import Tooltip from '../../../common/components/tooltip/Tooltip';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { cx } from '../../../common/utils/styleUtils';
import { tooltipDelayFast, tooltipDelayMid } from '../../../ontimeConfig';
import style from './TimeInputFlow.module.scss';
@@ -25,8 +24,17 @@ interface EventBlockTimerProps {
}
export default memo(TimeInputFlow);
function TimeInputFlow(props: EventBlockTimerProps) {
const { eventId, countToEnd, timeStart, timeEnd, duration, timeStrategy, linkStart, delay, showLabels } = props;
function TimeInputFlow({
eventId,
countToEnd,
timeStart,
timeEnd,
duration,
timeStrategy,
linkStart,
delay,
showLabels,
}: EventBlockTimerProps) {
const { updateEntry, updateTimer } = useEntryActions();
// In sync with EventEditorTimes
@@ -52,86 +60,80 @@ function TimeInputFlow(props: EventBlockTimerProps) {
}
const hasDelay = delay !== 0;
const isLockedEnd = timeStrategy === TimeStrategy.LockEnd;
const isLockedDuration = timeStrategy === TimeStrategy.LockDuration;
const activeStart = cx([style.timeAction, linkStart && style.active]);
const activeEnd = cx([style.timeAction, isLockedEnd && style.active]);
const activeDuration = cx([style.timeAction, isLockedDuration && style.active]);
return (
<>
<div>
{showLabels && <Editor.Label className={style.sectionTitle}>Start time</Editor.Label>}
<TimeInputWithButton<TimeField>
name='timeStart'
submitHandler={handleSubmit}
time={timeStart}
hasDelay={hasDelay}
placeholder='Start'
disabled={linkStart}
>
<Tooltip label='Link start to previous end' openDelay={tooltipDelayMid}>
<InputRightElement className={activeStart} onClick={() => handleLink(!linkStart)}>
<span className={style.timeLabel}>S</span>
<span className={style.fourtyfive}>{linkStart ? <IoLink /> : <IoUnlink />}</span>
</InputRightElement>
<div className={cx([style.inputGroup, hasDelay && style.delayed])}>
<TimeInput
name='timeStart'
submitHandler={handleSubmit}
time={timeStart}
placeholder='Start'
align='left'
disabled={linkStart}
/>
<Tooltip
text='Link start to previous end'
className={cx([style.timeAction, linkStart && style.active])}
onClick={() => handleLink(!linkStart)}
>
<span className={style.fourtyfive}>{linkStart ? <IoLink /> : <IoUnlink />}</span>
</Tooltip>
</TimeInputWithButton>
</div>
</div>
<div>
{showLabels && <Editor.Label>End time</Editor.Label>}
<TimeInputWithButton<TimeField>
name='timeEnd'
submitHandler={handleSubmit}
time={timeEnd}
hasDelay={hasDelay}
disabled={isLockedDuration}
placeholder='End'
>
<Tooltip label='Lock end' openDelay={tooltipDelayMid}>
<InputRightElement
className={activeEnd}
onClick={() => handleChangeStrategy(TimeStrategy.LockEnd)}
data-testid='lock__end'
>
<span className={style.timeLabel}>E</span>
{isLockedEnd ? <IoLockClosed /> : <IoLockOpenOutline />}
</InputRightElement>
<div className={cx([style.inputGroup, hasDelay && style.delayed])}>
<TimeInput
name='timeEnd'
submitHandler={handleSubmit}
time={timeEnd}
placeholder='End'
align='left'
disabled={isLockedDuration}
/>
<Tooltip
text='Lock end'
className={cx([style.timeAction, isLockedEnd && style.active])}
onClick={() => handleChangeStrategy(TimeStrategy.LockEnd)}
data-testid='lock__end'
>
{isLockedEnd ? <IoLockClosed /> : <IoLockOpenOutline />}
</Tooltip>
</TimeInputWithButton>
</div>
</div>
<div>
{showLabels && <Editor.Label>Duration</Editor.Label>}
<TimeInputWithButton<TimeField>
name='duration'
submitHandler={handleSubmit}
time={duration}
disabled={isLockedEnd}
placeholder='Duration'
>
<Tooltip label='Lock duration' openDelay={tooltipDelayMid}>
<InputRightElement
className={activeDuration}
onClick={() => handleChangeStrategy(TimeStrategy.LockDuration)}
data-testid='lock__duration'
>
<span className={style.timeLabel}>D</span>
{isLockedDuration ? <IoLockClosed /> : <IoLockOpenOutline />}
</InputRightElement>
<div className={cx([style.inputGroup, hasDelay && style.delayed])}>
<TimeInput
name='duration'
submitHandler={handleSubmit}
time={duration}
placeholder='Duration'
align='left'
disabled={isLockedEnd}
/>
<Tooltip
text='Lock duration'
className={cx([style.timeAction, isLockedDuration && style.active])}
onClick={() => handleChangeStrategy(TimeStrategy.LockDuration)}
data-testid='lock__duration'
>
{isLockedDuration ? <IoLockClosed /> : <IoLockOpenOutline />}
</Tooltip>
</TimeInputWithButton>
</div>
</div>
{warnings.length > 0 && (
<div className={style.timerNote} data-testid='event-warning'>
<Tooltip label={warnings.join(' - ')} openDelay={tooltipDelayFast} variant='ontime-ondark' shouldWrapChildren>
<IoAlertCircleOutline />
</Tooltip>
</div>
<Tooltip text={warnings.join(' - ')} className={style.timerNote} data-testid='event-warning' render={<span />}>
<IoAlertCircleOutline />
</Tooltip>
)}
</>
);
@@ -6,7 +6,7 @@ import { OntimeEntry, TimeField } from 'ontime-types';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { useFollowSelected } from '../../../common/hooks/useFollowComponent';
import { AppMode,sessionKeys } from '../../../ontimeConfig';
import { AppMode, sessionKeys } from '../../../ontimeConfig';
import { usePersistedCuesheetOptions } from '../cuesheet.options';
import CuesheetBody from './cuesheet-table-elements/CuesheetBody';
@@ -1,7 +1,6 @@
@import "../CuesheetTable.module.scss";
.eventRow {
vertical-align: top;
background: color-mix(in srgb, transparent 92%, var(--user-bg, $gray-500) 8%);
border-left: 4px solid var(--user-bg, $gray-500);
@@ -49,7 +49,8 @@ export default function EventRow({
const [cuesheetMode] = useSessionStorage<AppMode>({
key: sessionKeys.cuesheetMode,
defaultValue: AppMode.Edit,
}); const ownRef = useRef<HTMLTableRowElement>(null);
});
const ownRef = useRef<HTMLTableRowElement>(null);
const isVisible = useVisibleRowsStore((state) => state.visibleRows.has(rowId));
@@ -13,7 +13,8 @@
.about {
@extend .column;
font-size: calc(1rem - 2px);
font-size: calc(1rem - 3px);
padding-inline: 0.25rem;
}
.inline {
@@ -24,17 +24,12 @@ interface WelcomeProps {
export default function Welcome({ onClose }: WelcomeProps) {
const navigate = useNavigate();
/** handle cleanup actions before request closing the modal */
const handleClose = () => {
onClose();
};
/** handle loading a selected project */
const handleLoadProject = async (filename: string) => {
try {
await loadProject(filename);
await invalidateAllCaches();
handleClose();
onClose();
} catch (_error) {
/** no error handling for now */
}
@@ -45,7 +40,7 @@ export default function Welcome({ onClose }: WelcomeProps) {
try {
await loadDemo();
await invalidateAllCaches();
handleClose();
onClose();
} catch (_error) {
/** no error handling for now */
}
@@ -54,13 +49,13 @@ export default function Welcome({ onClose }: WelcomeProps) {
/** handle redirect to create modal */
const handleCallCreate = () => {
navigate('/editor?settings=project__create');
handleClose();
onClose();
};
return (
<Modal
isOpen
onClose={handleClose}
onClose={() => onClose()}
showBackdrop
bodyElements={
<div className={style.sections}>
@@ -74,7 +69,7 @@ export default function Welcome({ onClose }: WelcomeProps) {
<div className={style.column}>
<div className={style.header}>
Welcome to Ontime
<IconButton aria-label='close welcome modal' variant='subtle-white'>
<IconButton aria-label='close welcome modal' variant='subtle-white' onClick={() => onClose()}>
<IoClose />
</IconButton>
</div>
@@ -87,7 +82,7 @@ export default function Welcome({ onClose }: WelcomeProps) {
<th>Last Used</th>
</tr>
</thead>
<WelcomeProjectList loadProject={handleLoadProject} onClose={handleClose} />
<WelcomeProjectList loadProject={handleLoadProject} onClose={() => onClose()} />
</table>
</div>
</div>
@@ -97,7 +92,7 @@ export default function Welcome({ onClose }: WelcomeProps) {
<div className={style.column}>
<div className={style.buttonRow}>
<Button onClick={handleLoadDemo}>Load demo project</Button>
<ImportProjectButton onFinish={handleClose} />
<ImportProjectButton onFinish={() => onClose()} />
<Button variant='primary' onClick={handleCallCreate}>
Create new...
</Button>
+8 -7
View File
@@ -35,13 +35,14 @@ import { dispatchFromAdapter } from '../api-integration/integration.controller.j
import { generateId } from 'ontime-utils';
import { authenticateSocket } from '../middleware/authenticate.js';
type ClientId = string;
let instance: SocketServer | null = null;
class SocketServer implements IAdapter {
private readonly MAX_PAYLOAD = 1024 * 256; // 256Kb
private wss: WebSocketServer | null;
private readonly clients: Map<string, Client>;
private readonly clients: Map<ClientId, Client>;
private lastConnection: Date | null = null;
private shouldShowWelcome = true;
@@ -52,7 +53,7 @@ class SocketServer implements IAdapter {
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
instance = this;
this.clients = new Map<string, Client>();
this.clients = new Map<ClientId, Client>();
this.wss = null;
}
@@ -165,7 +166,7 @@ class SocketServer implements IAdapter {
};
}
private getOrCreateClient(clientId: string): Client {
private getOrCreateClient(clientId: ClientId): Client {
if (!this.clients.has(clientId)) {
this.clients.set(clientId, {
type: 'unknown',
@@ -183,11 +184,11 @@ class SocketServer implements IAdapter {
this.sendAsJson(MessageTag.ClientList, payload);
}
public getClientList(): string[] {
public getClientList(): ClientId[] {
return Array.from(this.clients.keys());
}
public renameClient(target: string, name: string) {
public renameClient(target: ClientId, name: string) {
const previousData = this.clients.get(target);
if (!previousData) {
throw new Error(`Client "${target}" not found`);
@@ -198,7 +199,7 @@ class SocketServer implements IAdapter {
this.sendClientList();
}
public redirectClient(target: string, path: string) {
public redirectClient(target: ClientId, path: string) {
const previousData = this.clients.get(target);
if (!previousData) {
throw new Error(`Client "${target}" not found`);
@@ -206,7 +207,7 @@ class SocketServer implements IAdapter {
this.sendAsJson(MessageTag.ClientRedirect, { target, path });
}
public identifyClient(target: string, identify: boolean) {
public identifyClient(target: ClientId, identify: boolean) {
const previousData = this.clients.get(target);
if (!previousData) {
throw new Error(`Client "${target}" not found`);
@@ -15,10 +15,10 @@ import { parseRundowns } from '../../api-data/rundown/rundown.parser.js';
import { getCurrentRundown, getProjectCustomFields } from '../../api-data/rundown/rundown.dao.js';
import { parseExcel } from '../../api-data/excel/excel.parser.js';
import { parseCustomFields } from '../../api-data/custom-fields/customFields.parser.js';
import { consoleSubdued } from '../../utils/console.js';
import { cellRequestFromEvent, type ClientSecret, getA1Notation, isClientSecret } from './sheetUtils.js';
import { catchCommonImportXlsxError } from './googleApi.utils.js';
import { consoleError, consoleSubdued } from '../../utils/console.js';
const sheetScope = 'https://www.googleapis.com/auth/spreadsheets';
const codesUrl = 'https://oauth2.googleapis.com/device/code';
+3 -3
View File
@@ -7,13 +7,13 @@ test('Aux timer buttons', async ({ page }) => {
await page.getByTestId('time-input-aux1').press('Enter');
await expect(page.getByTestId('time-input-aux1')).toHaveValue('12:34:56');
await page.getByTestId('aux-timer-start-1').click();
await expect(page.getByTestId('time-input-aux1')).toHaveValue('12:34:53', { timeout: 4000 });
await expect(page.getByTestId('time-label-aux1')).toHaveText('12:34:53', { timeout: 4000 });
await page.getByTestId('aux-timer-pause-1').click();
await expect(page.getByTestId('time-input-aux1')).toHaveValue('12:34:53');
await expect(page.getByTestId('time-label-aux1')).toHaveText('12:34:53');
await page.getByTestId('aux-timer-stop-1').click();
await expect(page.getByTestId('time-input-aux1')).toHaveValue('12:34:56');
await page.getByTestId('aux-timer-direction-1').click();
await page.getByTestId('aux-timer-start-1').click();
await expect(page.getByTestId('time-input-aux1')).toHaveValue('12:34:59', { timeout: 4000 });
await expect(page.getByTestId('time-label-aux1')).toHaveText('12:34:59', { timeout: 4000 });
await page.getByTestId('aux-timer-stop-1').click();
});
@@ -19,9 +19,9 @@ test('Copy-paste', async ({ page }) => {
await page.getByTestId('block__title').press('Enter');
// copy paste below
await page.locator('div').filter({ hasText: /^4$/ }).click();
await page.locator('div').filter({ hasText: /^4$/ }).press('Control+c');
await page.locator('div').filter({ hasText: /^4$/ }).press('Control+v');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '4' }).click();
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '4' }).press('Control+c');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '4' }).press('Control+v');
// assert
await expect(page.getByTestId('entry-2')).toBeVisible();
@@ -29,9 +29,9 @@ test('Copy-paste', async ({ page }) => {
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5');
// copy paste above
await page.locator('div').filter({ hasText: /^5$/ }).click();
await page.locator('div').filter({ hasText: /^5$/ }).press('Control+c');
await page.locator('div').filter({ hasText: /^5$/ }).press('Control+Shift+v');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '5' }).click();
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '5' }).press('Control+c');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '5' }).press('Control+Shift+v');
// assert
await expect(page.getByTestId('entry-2')).toBeVisible();
@@ -63,7 +63,7 @@ test('Move', async ({ page }) => {
// copy move up
await page.getByTestId('entry-3').getByTestId('rundown-event').getByText('3').click();
await page.getByTestId('entry-3').getByTestId('rundown-event').filter({ hasText: '3' }).press('Alt+Control+ArrowUp');
await page.getByTestId('entry-2').locator('div').filter({ hasText: /^3$/ }).press('Alt+Control+ArrowUp');
await page.getByTestId('entry-3').getByTestId('rundown-event').filter({ hasText: '3' }).press('Alt+Control+ArrowUp');
await expect(page.getByTestId('entry-1').getByTestId('rundown-event')).toContainText('3');
});
+1 -1
View File
@@ -28,7 +28,7 @@ test('show warning when event starts next day midnight', async ({ page }) => {
await page.getByRole('button', { name: 'Create Event' }).click();
await page.getByRole('button', { name: 'Event' }).nth(4).click();
await page.getByTestId('entry-2').getByText('E').click();
await page.getByTestId('entry-2').getByTestId('lock__end').click();
await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').click();
await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').fill('0');
await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').press('Enter');
+1 -1
View File
@@ -7,7 +7,7 @@ test('time until absolute', async ({ page }) => {
await page.getByRole('button', { name: 'Delete all' }).click();
await page.getByRole('button', { name: 'Create Event' }).click();
await page.getByRole('button', { name: 'Event' }).nth(4).click();
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
@@ -24,6 +24,8 @@ test('sheet file upload', async ({ page }) => {
await page.getByRole('button', { name: 'Import preview' }).click();
await page.getByRole('button', { name: 'Apply' }).click();
await page.getByRole('button', { name: 'Return' }).click();
await page.getByRole('button', { name: 'Close settings' }).scrollIntoViewIfNeeded();
await page.getByRole('button', { name: 'Close settings' }).click();
// asset test events