feat: edit duration in cuesheet

This commit is contained in:
Carlos Valente
2024-12-20 12:44:56 +01:00
committed by Carlos Valente
parent c445c42f8a
commit 88b114020b
8 changed files with 216 additions and 45 deletions
@@ -1,4 +1,4 @@
import { memo, useCallback, useRef } from 'react';
import { forwardRef, memo, useCallback, useImperativeHandle, useRef } from 'react';
import { Input } from '@chakra-ui/react';
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
@@ -6,19 +6,32 @@ import useReactiveTextInput from '../../../../common/components/input/text-input
interface SingleLineCellProps {
initialValue: string;
handleUpdate: (newValue: string) => void;
handleCancelUpdate?: () => void;
}
export default memo(SingleLineCell);
function SingleLineCell(props: SingleLineCellProps) {
const { initialValue, handleUpdate } = props;
const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
const { initialValue, handleUpdate, handleCancelUpdate } = props;
const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
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();
},
};
}, [ref]);
return (
<Input
ref={ref}
@@ -34,4 +47,8 @@ function SingleLineCell(props: SingleLineCellProps) {
autoComplete='off'
/>
);
}
});
SingleLineCell.displayName = 'SingleLineCell';
export default memo(SingleLineCell);
@@ -0,0 +1,16 @@
/* element attempts matching input styles */
.textInput {
padding-top: 0.25rem;
height: 2rem;
background-color: transparent;
border-radius: 3px;
&.muted {
color: $label-gray;
}
&:hover {
background-color: $gray-1100;
cursor: text;
}
}
@@ -0,0 +1,21 @@
import { HTMLAttributes, memo, PropsWithChildren } from 'react';
import { cx } from '../../../../common/utils/styleUtils';
import style from './TextLikeInput.module.scss';
export default memo(TextLikeInput);
interface TextLikeInputProps extends HTMLAttributes<HTMLSpanElement> {
muted?: boolean;
}
function TextLikeInput(props: PropsWithChildren<TextLikeInputProps>) {
const { muted, children, className, ...elementProps } = props;
const classes = cx([style.textInput, muted && style.muted, className]);
return (
<div className={classes} {...elementProps} tabIndex={0}>
{children}
</div>
);
}
@@ -0,0 +1,87 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { millisToString, parseUserTime } from 'ontime-utils';
import { formatDuration } from '../../../../common/utils/time';
import SingleLineCell from './SingleLineCell';
import TextLikeInput from './TextLikeInput';
interface TimeInputDurationProps {
initialValue: number;
lockedValue: boolean;
onSubmit: (value: string) => void;
}
export default function TimeInputDuration(props: TimeInputDurationProps) {
const { initialValue, lockedValue, onSubmit } = props;
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(initialValue);
const inputRef = useRef<HTMLInputElement>(null);
// when we go into edit mode, set focus to the input
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditing]);
// reset value when initialValue changes, avoiding interrupting the user if we are in edit mode
useEffect(() => {
if (!isEditing) {
setValue(initialValue);
}
}, [initialValue, isEditing]);
const handleFakeFocus = () => setIsEditing(true);
const handleFakeBlur = () => setIsEditing(false);
const handleUpdate = useCallback(
(newValue: string) => {
setIsEditing(false);
// Check if there is anything there
if (newValue === '') {
return;
}
// TODO: is this valid in the duration input?
// we dont know the values in the rundown, escalate to handler
if (newValue.startsWith('p') || newValue.startsWith('+')) {
onSubmit(newValue);
}
const valueInMillis = parseUserTime(newValue);
if (valueInMillis < 0 || isNaN(valueInMillis)) {
setValue(initialValue);
return;
}
if (valueInMillis === initialValue) {
return;
}
onSubmit(newValue);
setValue(Number(newValue));
},
[initialValue, onSubmit],
);
// duration times have a special format
const duration = formatDuration(value, false);
const timeString = millisToString(value);
return isEditing ? (
<SingleLineCell
ref={inputRef}
initialValue={timeString}
handleUpdate={handleUpdate}
handleCancelUpdate={handleFakeBlur}
/>
) : (
<TextLikeInput onClick={handleFakeFocus} onFocus={handleFakeFocus} muted={!lockedValue}>
{duration}
</TextLikeInput>
);
}
@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { CellContext, ColumnDef } from '@tanstack/react-table';
import { CustomFields, isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types';
import { CustomFields, isOntimeEvent, OntimeEvent, OntimeRundownEntry, TimeStrategy } from 'ontime-types';
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
import RunningTime from '../../../../features/viewers/common/running-time/RunningTime';
@@ -8,6 +8,7 @@ import { useCuesheetOptions } from '../../cuesheet.options';
import MultiLineCell from './MultiLineCell';
import SingleLineCell from './SingleLineCell';
import TimeInputDuration from './TimeInputDuration';
import style from '../CuesheetTable.module.scss';
@@ -27,11 +28,18 @@ function MakeTimer({ getValue, row: { original } }: CellContext<OntimeRundownEnt
);
}
function MakeDuration({ getValue }: CellContext<OntimeRundownEntry, unknown>) {
const { hideTableSeconds } = useCuesheetOptions();
const cellValue = (getValue() as number | null) ?? 0;
function MakeDuration({ getValue, row, table }: CellContext<OntimeRundownEntry, unknown>) {
const update = useCallback(
(newValue: string) => {
table.options.meta?.handleUpdateTimer(row.original.id, 'duration', newValue);
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
[row.original.id],
);
return <RunningTime value={cellValue} hideSeconds={hideTableSeconds} />;
const duration = (getValue() as number | null) ?? 0;
const isDurationLocked = (row.original as OntimeEvent)?.timeStrategy === TimeStrategy.LockDuration;
return <TimeInputDuration initialValue={duration} onSubmit={update} lockedValue={isDurationLocked} />;
}
function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {