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