refactor: simple migrations and tweaks to cuesheet

This commit is contained in:
Carlos Valente
2025-06-27 22:14:45 +02:00
committed by Carlos Valente
parent c8c2a05724
commit aefa4cbf44
23 changed files with 200 additions and 141 deletions
@@ -20,6 +20,11 @@
opacity: 0.4;
cursor: not-allowed;
}
&:focus-visible {
outline: 2px solid $blue-500;
outline-offset: 2px;
}
}
.small {
@@ -0,0 +1,50 @@
.checkbox {
box-sizing: border-box;
display: flex;
width: 1rem;
height: 1rem;
align-items: center;
justify-content: center;
border-radius: $component-border-radius-sm;
&[data-unchecked] {
background-color: $gray-1100;
&:hover:not(:disabled) {
background: $gray-1000;
}
&:active:not(:disabled) {
background: $gray-1200;
}
}
&[data-checked] {
background-color: $blue-500;
&:hover:not(:disabled) {
background: $blue-400;
}
&:active:not(:disabled) {
background: $blue-600;
}
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
&:focus-visible {
outline: 2px solid $blue-500;
outline-offset: 2px;
}
}
.indicator {
display: flex;
color: $ui-white;
&[data-unchecked] {
display: none;
}
}
@@ -0,0 +1,14 @@
import { IoCheckmark } from 'react-icons/io5';
import { Checkbox as BaseCheckbox } from '@base-ui-components/react/checkbox';
import style from './Checkbox.module.scss';
export default function Checkbox(checkboxProps: BaseCheckbox.Root.Props) {
return (
<BaseCheckbox.Root className={style.checkbox} {...checkboxProps}>
<BaseCheckbox.Indicator className={style.indicator}>
<IoCheckmark className={style.icon} />
</BaseCheckbox.Indicator>
</BaseCheckbox.Root>
);
}
@@ -1,7 +1,6 @@
.input {
box-sizing: border-box;
background-color: $gray-1200;
font-size: 1rem;
font-weight: 400;
color: $gray-200;
@@ -31,6 +30,15 @@
}
}
.subtle {
background-color: $gray-1200;
}
.ghosted {
background-color: transparent;
padding: 0;
}
.medium {
height: 2rem;
}
@@ -5,7 +5,7 @@ import { cx } from '../../../utils/styleUtils';
import style from './Input.module.scss';
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
variant?: 'subtle';
variant?: 'subtle' | 'ghosted';
height?: 'medium' | 'large';
fluid?: boolean;
}
@@ -20,6 +20,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
type='text'
autoCorrect='off'
autoComplete='off'
spellCheck='false'
className={cx([style.input, style[variant], style[height], fluid && style.fluid, className])}
{...inputProps}
/>
+23 -20
View File
@@ -1,7 +1,9 @@
import { useCallback, useMemo, useState } from 'react';
import { IoApps, IoSettingsOutline } from 'react-icons/io5';
import { IconButton, Modal, ModalContent, ModalOverlay, useDisclosure } from '@chakra-ui/react';
import { Modal, ModalContent, ModalOverlay } from '@chakra-ui/react';
import { useDisclosure } from '@mantine/hooks';
import IconButton from '../../common/components/buttons/IconButton';
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
import useViewEditor from '../../common/components/navigation-menu/useViewEditor';
import EmptyPage from '../../common/components/state/EmptyPage';
@@ -21,12 +23,11 @@ import { cuesheetOptions } from './cuesheet.options';
import styles from './CuesheetPage.module.scss';
export default function CuesheetPage() {
// TODO: can we use the normalised rundown for the table?
const { data: flatRundown, status: rundownStatus } = useFlatRundown();
const { data: customFields, status: customFieldStatus } = useCustomFields();
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable: true });
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure();
const [isMenuOpen, menuHandler] = useDisclosure();
const [isEventEditorOpen, eventEditorHandler] = useDisclosure();
const [eventId, setEventId] = useState<string | null>(null);
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]);
@@ -40,13 +41,13 @@ export default function CuesheetPage() {
(eventId: string | null) => {
if (eventId) {
setEventId(eventId);
onEventEditorOpen();
eventEditorHandler.open();
} else {
setEventId(null);
onEventEditorClose();
eventEditorHandler.close();
}
},
[onEventEditorClose, onEventEditorOpen],
[eventEditorHandler],
);
if (!customFields || !flatRundown || rundownStatus === 'pending' || customFieldStatus === 'pending') {
@@ -55,32 +56,34 @@ export default function CuesheetPage() {
return (
<>
<Modal isOpen={isEventEditorOpen} onClose={onEventEditorClose} variant='ontime'>
<Modal isOpen={isEventEditorOpen} onClose={eventEditorHandler.close} variant='ontime'>
<ModalOverlay />
<ModalContent maxWidth='max(640px, 40vw)' padding='1rem'>
<CuesheetEventEditor eventId={eventId!} />
</ModalContent>
</Modal>
<div className={styles.tableWrapper} data-testid='cuesheet'>
<NavigationMenu isOpen={isMenuOpen} onClose={onClose} />
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
<ViewParamsEditor viewOptions={cuesheetOptions} />
<CuesheetOverview>
<IconButton
aria-label='Toggle navigation'
variant='ontime-subtle-white'
size='lg'
icon={<IoApps />}
onClick={onOpen}
isDisabled={isViewLocked}
/>
variant='subtle-white'
size='xlarge'
onClick={menuHandler.open}
disabled={isViewLocked}
>
<IoApps />
</IconButton>
<IconButton
aria-label='Toggle settings'
variant='ontime-subtle-white'
size='lg'
icon={<IoSettingsOutline />}
variant='subtle-white'
size='xlarge'
onClick={showEditFormDrawer}
isDisabled={isViewLocked}
/>
disabled={isViewLocked}
>
<IoSettingsOutline />
</IconButton>
</CuesheetOverview>
<CuesheetProgress />
<CuesheetDnd columns={columns}>
@@ -17,9 +17,7 @@ interface CuesheetDndProps {
columns: ColumnDef<OntimeEntry>[];
}
export default function CuesheetDnd(props: PropsWithChildren<CuesheetDndProps>) {
const { columns, children } = props;
export default function CuesheetDnd({ columns, children }: PropsWithChildren<CuesheetDndProps>) {
const { columnOrder, saveColumnOrder } = useColumnManager(columns);
const sensors = useSensors(
@@ -21,9 +21,7 @@ interface CuesheetTableProps {
showModal: (eventId: MaybeString) => void;
}
export default function CuesheetTable(props: CuesheetTableProps) {
const { data, columns, showModal } = props;
export default function CuesheetTable({ data, columns, showModal }: CuesheetTableProps) {
const { updateEntry, updateTimer } = useEntryActions();
const { followSelected, showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
@@ -10,8 +10,7 @@ interface BlockRowProps {
columnCount: number;
}
function BlockRow(props: BlockRowProps) {
const { hidePast, title, columnCount } = props;
function BlockRow({ hidePast, title, columnCount }: BlockRowProps) {
const { currentBlockId } = useCurrentBlockId();
const firstCellRef = useRef<null | HTMLTableCellElement>(null);
@@ -18,9 +18,7 @@ interface CuesheetBodyProps {
table: Table<OntimeEntry>;
}
export default function CuesheetBody(props: CuesheetBodyProps) {
const { rowModel, selectedRef, table } = props;
export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetBodyProps) {
const { selectedEventId } = useSelectedEventId();
const { hideDelays, hidePast } = useCuesheetOptions();
@@ -13,8 +13,7 @@ interface CuesheetHeaderProps {
headerGroups: HeaderGroup<OntimeEntry>[];
}
export default function CuesheetHeader(props: CuesheetHeaderProps) {
const { headerGroups } = props;
export default function CuesheetHeader({ headerGroups }: CuesheetHeaderProps) {
const { hideIndexColumn, showActionMenu } = useCuesheetOptions();
return (
@@ -8,8 +8,7 @@ interface DelayRowProps {
duration: number;
}
function DelayRow(props: DelayRowProps) {
const { duration } = props;
function DelayRow({ duration }: DelayRowProps) {
const delayTime = millisToDelayString(duration, 'expanded');
return (
@@ -17,9 +17,13 @@ interface ParentFocusableInput extends HTMLInputElement {
export default memo(DurationInput);
function DurationInput(props: PropsWithChildren<DurationInputProps>) {
const { initialValue, lockedValue, delayed, onSubmit, children } = props;
function DurationInput({
initialValue,
lockedValue,
delayed,
onSubmit,
children,
}: PropsWithChildren<DurationInputProps>) {
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(initialValue);
const inputRef = useRef<ParentFocusableInput>(null);
@@ -1,5 +1,6 @@
import { memo } from 'react';
import { Input } from '@chakra-ui/react';
import Input from '../../../../common/components/input/input/Input';
import style from './EditableImage.module.scss';
@@ -10,9 +11,7 @@ interface EditableImageProps {
export default memo(EditableImage);
function EditableImage(props: EditableImageProps) {
const { initialValue, updateValue } = props;
function EditableImage({ initialValue, updateValue }: EditableImageProps) {
const handleUpdate = (newValue: string) => {
if (newValue === initialValue) {
return;
@@ -26,10 +25,8 @@ function EditableImage(props: EditableImageProps) {
if (!initialValue) {
return (
<Input
size='sm'
variant='ontime-transparent'
padding={0}
fontSize='md'
variant='ghosted'
fluid
placeholder='Paste image URL'
onBlur={(event) => handleUpdate(event.currentTarget.value)}
onKeyDown={(event) => {
@@ -38,8 +35,6 @@ function EditableImage(props: EditableImageProps) {
}
}}
defaultValue={initialValue}
spellCheck={false}
autoComplete='off'
/>
);
}
@@ -39,8 +39,7 @@ export default memo(EventRow, (prevProps, nextProps) => {
);
});
function EventRow(props: EventRowProps) {
const { rowId, event, eventIndex, rowIndex, isPast, selectedRef, rowBgColour, table } = props;
function EventRow({ rowId, event, eventIndex, rowIndex, isPast, selectedRef, rowBgColour, table }: EventRowProps) {
const { hideIndexColumn, showActionMenu } = useCuesheetOptions();
const ownRef = useRef<HTMLTableRowElement>(null);
const [isVisible, setIsVisible] = useState(false);
@@ -10,8 +10,7 @@ interface MultiLineCellProps {
export default memo(MultiLineCell);
function MultiLineCell(props: MultiLineCellProps) {
const { initialValue, handleUpdate } = props;
function MultiLineCell({ initialValue, handleUpdate }: MultiLineCellProps) {
const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
@@ -1,6 +1,6 @@
import { forwardRef, memo, useCallback, useImperativeHandle, useRef } from 'react';
import { Input } from '@chakra-ui/react';
import Input from '../../../../common/components/input/input/Input';
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
interface SingleLineCellProps {
@@ -10,50 +10,47 @@ interface SingleLineCellProps {
handleCancelUpdate?: () => void;
}
const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
const { initialValue, allowSubmitSameValue, handleUpdate, handleCancelUpdate } = props;
const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
const SingleLineCell = forwardRef(
({ initialValue, allowSubmitSameValue, handleUpdate, handleCancelUpdate }: SingleLineCellProps, inputRef) => {
const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
allowSubmitSameValue,
allowKeyboardNavigation: true,
submitOnEnter: true, // single line should submit on enter
submitOnCtrlEnter: true,
onCancelUpdate: handleCancelUpdate,
});
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
allowSubmitSameValue,
allowKeyboardNavigation: true,
submitOnEnter: true, // single line should submit on enter
submitOnCtrlEnter: true,
onCancelUpdate: handleCancelUpdate,
});
// expose a subset of the methods to the parent
useImperativeHandle(inputRef, () => {
return {
focus() {
ref.current?.focus();
},
select() {
ref.current?.select();
},
focusParentElement() {
ref.current?.parentElement?.focus();
},
};
}, [ref]);
// expose a subset of the methods to the parent
useImperativeHandle(inputRef, () => {
return {
focus() {
ref.current?.focus();
},
select() {
ref.current?.select();
},
focusParentElement() {
ref.current?.parentElement?.focus();
},
};
}, [ref]);
return (
<Input
ref={ref}
size='sm'
variant='ontime-transparent'
padding={0}
fontSize='md'
value={value}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
spellCheck={false}
autoComplete='off'
/>
);
});
return (
<Input
ref={ref}
variant='ghosted'
fluid
value={value}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
/>
);
},
);
SingleLineCell.displayName = 'SingleLineCell';
@@ -18,9 +18,9 @@ const TextLikeInput = forwardRef((props: PropsWithChildren<TextLikeInputProps>,
return {
focusParentElement() {
ref.current?.parentElement?.focus();
}
}
})
},
};
});
return (
<div className={classes} {...elementProps} tabIndex={0} ref={ref}>
@@ -19,9 +19,13 @@ interface ParentFocusableInput extends HTMLInputElement {
export default memo(TimeInputDuration);
function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
const { initialValue, lockedValue, delayed, onSubmit, children } = props;
function TimeInputDuration({
initialValue,
lockedValue,
delayed,
onSubmit,
children,
}: PropsWithChildren<TimeInputDurationProps>) {
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(initialValue);
const inputRef = useRef<ParentFocusableInput>(null);
@@ -10,8 +10,7 @@ interface CuesheetTableMenuProps {
export default memo(CuesheetTableMenu);
function CuesheetTableMenu(props: CuesheetTableMenuProps) {
const { showModal } = props;
function CuesheetTableMenu({ showModal }: CuesheetTableMenuProps) {
const { isOpen, eventId, entryIndex, position, closeMenu } = useCuesheetTableMenu();
return (
@@ -11,8 +11,7 @@ interface CuesheetTableMenuActionsProps {
showModal: (entryId: string) => void;
}
export default function CuesheetTableMenuActions(props: CuesheetTableMenuActionsProps) {
const { eventId, entryIndex, showModal } = props;
export default function CuesheetTableMenuActions({ eventId, entryIndex, showModal }: CuesheetTableMenuActionsProps) {
const { addEntry, getEntryById, move, deleteEntry } = useEntryActions();
const handleCloneEvent = () => {
@@ -1,18 +1,13 @@
import { memo, ReactNode } from 'react';
import { Button, Checkbox } from '@chakra-ui/react';
import { Column } from '@tanstack/react-table';
import { OntimeEntry } from 'ontime-types';
import Button from '../../../../common/components/buttons/Button';
import Checkbox from '../../../../common/components/checkbox/Checkbox';
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
import style from './CuesheetTableSettings.module.scss';
// reusable button styles
const buttonProps = {
size: 'xs',
variant: 'ontime-subtle',
};
interface CuesheetTableSettingsProps {
columns: Column<OntimeEntry, unknown>[];
handleResetResizing: () => void;
@@ -20,9 +15,12 @@ interface CuesheetTableSettingsProps {
handleClearToggles: () => void;
}
function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
const { columns, handleResetResizing, handleResetReordering, handleClearToggles } = props;
function CuesheetTableSettings({
columns,
handleResetResizing,
handleResetReordering,
handleClearToggles,
}: CuesheetTableSettingsProps) {
return (
<div className={style.tableSettings}>
<div>
@@ -32,14 +30,10 @@ function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
const columnHeader = column.columnDef.header;
const visible = column.getIsVisible();
return (
<label key={`${column.id}-${visible}`} className={style.option}>
<Checkbox
variant='ontime-ondark'
defaultChecked={visible}
onChange={column.getToggleVisibilityHandler()}
/>
<Editor.Label key={`${column.id}-${visible}`} className={style.option}>
<Checkbox defaultChecked={visible} onCheckedChange={column.toggleVisibility} />
{columnHeader as ReactNode}
</label>
</Editor.Label>
);
})}
</div>
@@ -47,13 +41,13 @@ function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
<div className={style.column}>
<Editor.Label className={style.sectionTitle}>Reset Options</Editor.Label>
<div className={style.row}>
<Button onClick={handleClearToggles} {...buttonProps}>
<Button size='small' variant='subtle' onClick={handleClearToggles}>
Show All
</Button>
<Button onClick={handleResetResizing} {...buttonProps}>
<Button size='small' variant='subtle' onClick={handleResetResizing}>
Reset Resizing
</Button>
<Button onClick={handleResetReordering} {...buttonProps}>
<Button size='small' variant='subtle' onClick={handleResetReordering}>
Reset Reordering
</Button>
</div>
+12 -15
View File
@@ -1,8 +1,8 @@
import { lazy, useCallback, useEffect } from 'react';
import { IoApps, IoClose, IoSettingsOutline } from 'react-icons/io5';
import { IconButton, useDisclosure } from '@chakra-ui/react';
import { useHotkeys } from '@mantine/hooks';
import { useDisclosure, useHotkeys } from '@mantine/hooks';
import IconButton from '../../common/components/buttons/IconButton';
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
import { useElectronListener } from '../../common/hooks/useElectronEvent';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
@@ -20,7 +20,7 @@ const MessageControl = lazy(() => import('../../features/control/message/Message
export default function Editor() {
const { isOpen: isSettingsOpen, setLocation, close } = useAppSettingsNavigation();
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
const [isOpen, handler] = useDisclosure();
useWindowTitle('Editor');
@@ -49,22 +49,19 @@ export default function Editor() {
return (
<div className={styles.mainContainer} data-testid='event-editor'>
<WelcomePlacement />
<NavigationMenu isOpen={isMenuOpen} onClose={onClose} />
<NavigationMenu isOpen={isOpen} onClose={handler.close} />
<EditorOverview>
<IconButton
aria-label='Toggle navigation'
variant='ontime-subtle-white'
size='lg'
icon={<IoApps />}
onClick={onOpen}
/>
<IconButton aria-label='Toggle navigation' variant='subtle-white' size='xlarge' onClick={handler.open}>
<IoApps />
</IconButton>
<IconButton
aria-label='Toggle settings'
variant={isSettingsOpen ? 'ontime-subtle' : 'ontime-subtle-white'}
size='lg'
icon={isSettingsOpen ? <IoClose /> : <IoSettingsOutline />}
variant={isSettingsOpen ? 'subtle' : 'subtle-white'}
size='xlarge'
onClick={toggleSettings}
/>
>
{isSettingsOpen ? <IoClose /> : <IoSettingsOutline />}
</IconButton>
</EditorOverview>
{isSettingsOpen ? (
<AppSettings />