mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 06:59:09 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b8885d0b7 | |||
| dd8cfef4de | |||
| bdd4f75c4a | |||
| 3a740ec713 | |||
| dcdfd9ca85 | |||
| b946b245c2 | |||
| d17a7e9321 | |||
| c960a983a0 | |||
| 7498abb326 | |||
| a0d72d1776 | |||
| eab63e838d | |||
| c238147b75 |
@@ -8,9 +8,12 @@
|
|||||||
padding-top: 10vh;
|
padding-top: 10vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.inline {
|
||||||
width: 100%;
|
display: flex;
|
||||||
opacity: 0.8;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
import EmptyImage from '../../../assets/images/empty.svg?react';
|
import { SupportedEntry } from 'ontime-types';
|
||||||
|
import { IoAdd } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
|
import Button from '../buttons/Button';
|
||||||
|
import Empty from './Empty';
|
||||||
|
|
||||||
import style from './EmptyTableBody.module.scss';
|
import style from './EmptyTableBody.module.scss';
|
||||||
|
|
||||||
interface EmptyTableBodyProps {
|
interface EmptyTableBodyProps {
|
||||||
text: string;
|
handleAddNew?: (type: SupportedEntry) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmptyTableBody({ text }: EmptyTableBodyProps) {
|
export default function EmptyTableBody({ handleAddNew }: EmptyTableBodyProps) {
|
||||||
|
const { getLocalizedString } = useTranslation();
|
||||||
|
const text = getLocalizedString('common.no_data');
|
||||||
return (
|
return (
|
||||||
<tbody className={style.emptyContainer}>
|
<tbody className={style.emptyContainer}>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={99} className={style.emptyCell}>
|
<td colSpan={99} className={style.emptyCell}>
|
||||||
<EmptyImage className={style.empty} />
|
<Empty injectedStyles={{ marginTop: '5vh' }} />
|
||||||
{text && <span className={style.text}>{text}</span>}
|
<span className={style.text}>{text}</span>
|
||||||
|
{handleAddNew && (
|
||||||
|
<div className={style.inline}>
|
||||||
|
<Button onClick={() => handleAddNew(SupportedEntry.Event)} variant='primary' size='large'>
|
||||||
|
<IoAdd />
|
||||||
|
Create Event
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export default function ManageRundowns() {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
render={<IconButton variant='ghosted-white' />}
|
render={<IconButton variant='ghosted-white' data-testId='rundown_menu' />}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTableNav } from '@table-nav/react';
|
import { useTableNav } from '@table-nav/react';
|
||||||
import { ColumnDef, Table, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
import { ColumnDef, Table, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
import { OntimeEntry, TimeField, isOntimeDelay, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
import { OntimeEntry, SupportedEntry, TimeField, isOntimeDelay, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
||||||
import { ComponentProps, ReactNode, memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
import { ComponentProps, ReactNode, memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
ContextProp,
|
ContextProp,
|
||||||
@@ -20,6 +20,7 @@ import { usePersistedRundownOptions } from '../../../features/rundown/rundown.op
|
|||||||
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
||||||
import { AppMode } from '../../../ontimeConfig';
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
||||||
|
import { useCuesheetPermissions } from '../useTablePermissions';
|
||||||
import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader';
|
import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader';
|
||||||
import DelayRow from './cuesheet-table-elements/DelayRow';
|
import DelayRow from './cuesheet-table-elements/DelayRow';
|
||||||
import EventRow from './cuesheet-table-elements/EventRow';
|
import EventRow from './cuesheet-table-elements/EventRow';
|
||||||
@@ -62,7 +63,8 @@ export default function CuesheetTable({
|
|||||||
insertElement,
|
insertElement,
|
||||||
}: CuesheetTableProps) {
|
}: CuesheetTableProps) {
|
||||||
const { flatRundown, status, selectedEventId } = source;
|
const { flatRundown, status, selectedEventId } = source;
|
||||||
const { updateEntry, updateTimer } = useEntryActionsContext();
|
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
|
||||||
|
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
|
||||||
|
|
||||||
const useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions;
|
const useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions;
|
||||||
const optionsStore = useOptions();
|
const optionsStore = useOptions();
|
||||||
@@ -202,8 +204,9 @@ export default function CuesheetTable({
|
|||||||
listeners,
|
listeners,
|
||||||
rows,
|
rows,
|
||||||
table,
|
table,
|
||||||
|
handleAddNew: canCreateEntries ? (type: SupportedEntry) => addEntry({ type }) : undefined,
|
||||||
}),
|
}),
|
||||||
[columnSizeVars, cursor, listeners, rows, table],
|
[columnSizeVars, cursor, listeners, rows, table, addEntry, canCreateEntries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const computeItemKey = useCallback((_: number, item: ExtendedEntry) => item.id, []);
|
const computeItemKey = useCallback((_: number, item: ExtendedEntry) => item.id, []);
|
||||||
@@ -273,10 +276,13 @@ interface CuesheetVirtuosoContext {
|
|||||||
listeners: ReturnType<typeof useTableNav>['listeners'];
|
listeners: ReturnType<typeof useTableNav>['listeners'];
|
||||||
rows: ReturnType<Table<ExtendedEntry>['getRowModel']>['rows'];
|
rows: ReturnType<Table<ExtendedEntry>['getRowModel']>['rows'];
|
||||||
table: Table<ExtendedEntry>;
|
table: Table<ExtendedEntry>;
|
||||||
|
handleAddNew?: (type: SupportedEntry) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmptyPlaceholder = memo(function EmptyPlaceholder() {
|
const EmptyPlaceholder = memo(function EmptyPlaceholder({
|
||||||
return <EmptyTableBody text='No data in rundown' />;
|
context,
|
||||||
|
}: TableProps & ContextProp<CuesheetVirtuosoContext>) {
|
||||||
|
return <EmptyTableBody handleAddNew={context.handleAddNew} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
const CuesheetTableElement = memo(function CuesheetTableElement({
|
const CuesheetTableElement = memo(function CuesheetTableElement({
|
||||||
|
|||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import MultiLineCell from './MultiLineCell';
|
||||||
|
import SingleLineCell from './SingleLineCell';
|
||||||
|
import TextLikeInput from './TextLikeInput';
|
||||||
|
|
||||||
|
interface EditableCellProps {
|
||||||
|
initialValue: string;
|
||||||
|
multiline?: boolean;
|
||||||
|
fieldId?: string;
|
||||||
|
fieldLabel?: string;
|
||||||
|
handleUpdate: (newValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FocusableEditor {
|
||||||
|
focus: () => void;
|
||||||
|
select?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FocusableDisplay {
|
||||||
|
focusParentElement: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily mounts the text editor for a cell.
|
||||||
|
*
|
||||||
|
* Mounting an `<input>`/`<textarea>` editor (with its reactive-input hooks and autosize) for every
|
||||||
|
* cell is expensive when many rows mount at once during virtualised scroll. While the cell is not
|
||||||
|
* being edited we render a lightweight, focusable display element and only mount the real editor
|
||||||
|
* when the user clicks/focuses the cell — mirroring how the time/duration cells already behave.
|
||||||
|
*
|
||||||
|
* On exit we return focus to the parent cell (through the display element, in a layout effect once
|
||||||
|
* it is back in the DOM) so the table keyboard navigation keeps working — the editor is unmounted
|
||||||
|
* by then, so we cannot rely on its own ref.
|
||||||
|
*/
|
||||||
|
function EditableCell({ initialValue, multiline, fieldId, fieldLabel, handleUpdate }: EditableCellProps) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const wasEditing = useRef(false);
|
||||||
|
const editorRef = useRef<FocusableEditor | null>(null);
|
||||||
|
const displayRef = useRef<FocusableDisplay | null>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (isEditing) {
|
||||||
|
// focus the editor once it mounts on entering edit mode
|
||||||
|
editorRef.current?.focus();
|
||||||
|
editorRef.current?.select?.();
|
||||||
|
} else if (wasEditing.current) {
|
||||||
|
// returning from edit: hand focus back to the cell so table keyboard navigation continues
|
||||||
|
displayRef.current?.focusParentElement();
|
||||||
|
}
|
||||||
|
wasEditing.current = isEditing;
|
||||||
|
}, [isEditing]);
|
||||||
|
|
||||||
|
const enterEdit = useCallback(() => setIsEditing(true), []);
|
||||||
|
const exitEdit = useCallback(() => setIsEditing(false), []);
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
setIsEditing(false);
|
||||||
|
handleUpdate(newValue);
|
||||||
|
},
|
||||||
|
[handleUpdate],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isEditing) {
|
||||||
|
return (
|
||||||
|
<TextLikeInput
|
||||||
|
ref={displayRef}
|
||||||
|
onClick={enterEdit}
|
||||||
|
onFocus={enterEdit}
|
||||||
|
multiline={multiline}
|
||||||
|
topAligned
|
||||||
|
aria-label={fieldLabel ? `${fieldLabel} cell` : undefined}
|
||||||
|
>
|
||||||
|
{initialValue}
|
||||||
|
</TextLikeInput>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiline ? (
|
||||||
|
<MultiLineCell
|
||||||
|
ref={editorRef}
|
||||||
|
initialValue={initialValue}
|
||||||
|
fieldId={fieldId}
|
||||||
|
fieldLabel={fieldLabel}
|
||||||
|
handleUpdate={onSubmit}
|
||||||
|
handleCancelUpdate={exitEdit}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SingleLineCell
|
||||||
|
ref={editorRef}
|
||||||
|
initialValue={initialValue}
|
||||||
|
fieldId={fieldId}
|
||||||
|
fieldLabel={fieldLabel}
|
||||||
|
handleUpdate={onSubmit}
|
||||||
|
handleCancelUpdate={exitEdit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(EditableCell);
|
||||||
+24
-5
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useRef } from 'react';
|
import { forwardRef, memo, useCallback, useImperativeHandle, useRef } from 'react';
|
||||||
|
|
||||||
import { AutoTextarea } from '../../../../common/components/input/auto-textarea/AutoTextarea';
|
import { AutoTextarea } from '../../../../common/components/input/auto-textarea/AutoTextarea';
|
||||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
@@ -8,19 +8,36 @@ interface MultiLineCellProps {
|
|||||||
fieldId?: string;
|
fieldId?: string;
|
||||||
fieldLabel?: string;
|
fieldLabel?: string;
|
||||||
handleUpdate: (newValue: string) => void;
|
handleUpdate: (newValue: string) => void;
|
||||||
|
handleCancelUpdate?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(MultiLineCell);
|
const MultiLineCell = forwardRef(function MultiLineCell(
|
||||||
|
{ initialValue, fieldId, fieldLabel, handleUpdate, handleCancelUpdate }: MultiLineCellProps,
|
||||||
function MultiLineCell({ initialValue, fieldId, fieldLabel, handleUpdate }: MultiLineCellProps) {
|
inputRef,
|
||||||
|
) {
|
||||||
const ref = useRef<HTMLTextAreaElement | null>(null);
|
const ref = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||||
|
|
||||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||||
submitOnCtrlEnter: true,
|
submitOnCtrlEnter: true,
|
||||||
allowKeyboardNavigation: true,
|
allowKeyboardNavigation: true,
|
||||||
|
onCancelUpdate: handleCancelUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// expose focus to the parent so the editor can be focused when mounted on demand
|
||||||
|
useImperativeHandle(
|
||||||
|
inputRef,
|
||||||
|
() => ({
|
||||||
|
focus() {
|
||||||
|
ref.current?.focus();
|
||||||
|
},
|
||||||
|
select() {
|
||||||
|
ref.current?.select();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[ref],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoTextarea
|
<AutoTextarea
|
||||||
inputref={ref}
|
inputref={ref}
|
||||||
@@ -36,4 +53,6 @@ function MultiLineCell({ initialValue, fieldId, fieldLabel, handleUpdate }: Mult
|
|||||||
aria-label={fieldLabel ? `${fieldLabel} editor` : undefined}
|
aria-label={fieldLabel ? `${fieldLabel} editor` : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default memo(MultiLineCell);
|
||||||
|
|||||||
+20
-1
@@ -1,6 +1,8 @@
|
|||||||
/* element matching input styles */
|
/* element matching input styles */
|
||||||
.textInput {
|
.textInput {
|
||||||
height: 2rem;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 2rem;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-radius: $component-border-radius-md;
|
border-radius: $component-border-radius-md;
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
@@ -32,4 +34,21 @@
|
|||||||
background-color: $gray-1100;
|
background-color: $gray-1100;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// single-line placeholder: keep the glyph in a 2rem band at the top of the cell so it lines up
|
||||||
|
// with the editor (which mounts top-aligned) and does not jump when the cell is taller than 2rem
|
||||||
|
&.topAligned:not(.multiline) {
|
||||||
|
align-items: flex-start;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multiline {
|
||||||
|
height: auto;
|
||||||
|
min-height: 100%; // fill the cell so the whole area is clickable (grows with content)
|
||||||
|
text-wrap: wrap;
|
||||||
|
white-space: break-spaces;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 0.25em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -8,11 +8,23 @@ interface TextLikeInputProps extends HTMLAttributes<HTMLSpanElement> {
|
|||||||
offset?: 'over' | 'under' | 'muted' | null;
|
offset?: 'over' | 'under' | 'muted' | null;
|
||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
multiline?: boolean;
|
||||||
|
/** keep the content at the top of the cell (matches an editor that mounts top-aligned) */
|
||||||
|
topAligned?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextLikeInput = forwardRef(
|
const TextLikeInput = forwardRef(
|
||||||
(
|
(
|
||||||
{ offset, muted, disabled, children, className, ...elementProps }: PropsWithChildren<TextLikeInputProps>,
|
{
|
||||||
|
offset,
|
||||||
|
muted,
|
||||||
|
disabled,
|
||||||
|
multiline,
|
||||||
|
topAligned,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...elementProps
|
||||||
|
}: PropsWithChildren<TextLikeInputProps>,
|
||||||
textRef,
|
textRef,
|
||||||
) => {
|
) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -21,6 +33,8 @@ const TextLikeInput = forwardRef(
|
|||||||
offset && style[offset],
|
offset && style[offset],
|
||||||
muted && style.muted,
|
muted && style.muted,
|
||||||
disabled && style.disabled,
|
disabled && style.disabled,
|
||||||
|
multiline && style.multiline,
|
||||||
|
topAligned && style.topAligned,
|
||||||
className,
|
className,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -9,12 +9,11 @@ import { formatDuration, formatTime } from '../../../../common/utils/time';
|
|||||||
import { AppMode } from '../../../../ontimeConfig';
|
import { AppMode } from '../../../../ontimeConfig';
|
||||||
import { getCuesheetColumnAccessPolicy } from '../../cuesheet.policies';
|
import { getCuesheetColumnAccessPolicy } from '../../cuesheet.policies';
|
||||||
import DurationInput from './DurationInput';
|
import DurationInput from './DurationInput';
|
||||||
|
import EditableCell from './EditableCell';
|
||||||
import EditableImage from './EditableImage';
|
import EditableImage from './EditableImage';
|
||||||
import FlagCell from './FlagCell';
|
import FlagCell from './FlagCell';
|
||||||
import GhostedText from './GhostedText';
|
import GhostedText from './GhostedText';
|
||||||
import MultiLineCell from './MultiLineCell';
|
|
||||||
import MutedText from './MutedText';
|
import MutedText from './MutedText';
|
||||||
import SingleLineCell from './SingleLineCell';
|
|
||||||
import TimeInput from './TimeInput';
|
import TimeInput from './TimeInput';
|
||||||
|
|
||||||
function getColumnLabel(column: CellContext<ExtendedEntry, unknown>['column']): string {
|
function getColumnLabel(column: CellContext<ExtendedEntry, unknown>['column']): string {
|
||||||
@@ -151,7 +150,8 @@ function MakeMultiLineField({ row, column, table }: CellContext<ExtendedEntry, u
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiLineCell
|
<EditableCell
|
||||||
|
multiline
|
||||||
initialValue={initialValue as string}
|
initialValue={initialValue as string}
|
||||||
fieldId={column.id}
|
fieldId={column.id}
|
||||||
fieldLabel={getColumnLabel(column)}
|
fieldLabel={getColumnLabel(column)}
|
||||||
@@ -198,7 +198,7 @@ function MakeSingleLineField({ row, column, table }: CellContext<ExtendedEntry,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleLineCell
|
<EditableCell
|
||||||
initialValue={initialValue as string}
|
initialValue={initialValue as string}
|
||||||
fieldId={column.id}
|
fieldId={column.id}
|
||||||
fieldLabel={getColumnLabel(column)}
|
fieldLabel={getColumnLabel(column)}
|
||||||
@@ -238,7 +238,8 @@ function MakeCustomField({ row, column, table }: CellContext<ExtendedEntry, unkn
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiLineCell
|
<EditableCell
|
||||||
|
multiline
|
||||||
initialValue={initialValue}
|
initialValue={initialValue}
|
||||||
fieldId={column.id}
|
fieldId={column.id}
|
||||||
fieldLabel={getColumnLabel(column)}
|
fieldLabel={getColumnLabel(column)}
|
||||||
|
|||||||
@@ -57,46 +57,78 @@ test('cuesheet datagrid keeps keyboard focus flow while editing text cells', asy
|
|||||||
const firstEvent = page.getByTestId('cuesheet-event').first();
|
const firstEvent = page.getByTestId('cuesheet-event').first();
|
||||||
await expect(firstEvent).toBeVisible();
|
await expect(firstEvent).toBeVisible();
|
||||||
|
|
||||||
|
const cueCell = firstEvent.getByTestId('cuesheet-cell-cue');
|
||||||
|
const titleCell = firstEvent.getByTestId('cuesheet-cell-title');
|
||||||
|
const noteCell = firstEvent.getByTestId('cuesheet-cell-note');
|
||||||
const cueEditor = firstEvent.getByTestId('cuesheet-editor-cue');
|
const cueEditor = firstEvent.getByTestId('cuesheet-editor-cue');
|
||||||
const titleEditor = firstEvent.getByTestId('cuesheet-editor-title');
|
const titleEditor = firstEvent.getByTestId('cuesheet-editor-title');
|
||||||
const noteEditor = firstEvent.getByTestId('cuesheet-editor-note');
|
const noteEditor = firstEvent.getByTestId('cuesheet-editor-note');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. focus a cell in the datagrid single line text
|
* 1. clicking a single line text cell opens the editor (mounted on demand)
|
||||||
* submitting the data returns the focus to the parent
|
* submitting with Enter closes the editor and returns focus to the parent cell
|
||||||
*/
|
*/
|
||||||
await titleEditor.click();
|
await titleCell.click();
|
||||||
await expect(titleEditor).toBeFocused();
|
await expect(titleEditor).toBeFocused();
|
||||||
const updatedTitle = `focus-title-${Date.now()}`;
|
const updatedTitle = `focus-title-${Date.now()}`;
|
||||||
await titleEditor.fill(updatedTitle);
|
await titleEditor.fill(updatedTitle);
|
||||||
await titleEditor.press('Enter');
|
await titleEditor.press('Enter');
|
||||||
await expect(titleEditor).not.toBeFocused();
|
await expect(titleEditor).toHaveCount(0);
|
||||||
await expect(titleEditor).toHaveValue(updatedTitle);
|
await expect(titleCell).toContainText(updatedTitle);
|
||||||
|
await expect(titleCell).toBeFocused();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2. navigate and modify multiline text cell
|
* 2. navigate to the multiline text cell with the keyboard and open it with Enter
|
||||||
* submitting works with ctrl/cmd + enter and the focus returns to the parent
|
* submitting works with ctrl/cmd + enter and the focus returns to the parent
|
||||||
*/
|
*/
|
||||||
await page.keyboard.press('ArrowRight');
|
await page.keyboard.press('ArrowRight');
|
||||||
|
await expect(noteCell).toBeFocused();
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await expect(noteEditor).toBeFocused();
|
await expect(noteEditor).toBeFocused();
|
||||||
const updatedNote = `focus-note-${Date.now()}`;
|
const updatedNote = `focus-note-${Date.now()}`;
|
||||||
await noteEditor.fill(updatedNote);
|
await noteEditor.fill(updatedNote);
|
||||||
await noteEditor.press('ControlOrMeta+Enter');
|
await noteEditor.press('ControlOrMeta+Enter');
|
||||||
await expect(noteEditor).not.toBeFocused();
|
await expect(noteEditor).toHaveCount(0);
|
||||||
await expect(noteEditor).toHaveValue(updatedNote);
|
await expect(noteCell).toContainText(updatedNote);
|
||||||
|
await expect(noteCell).toBeFocused();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2. navigate and modify single line text cell again
|
* 3. navigating back returns focus to the title cell
|
||||||
* pressing escape cancels the edit and the focus returns to the parent
|
* opening the cue cell and pressing escape cancels the edit and reverts the value
|
||||||
*/
|
*/
|
||||||
await page.keyboard.press('ArrowLeft');
|
await page.keyboard.press('ArrowLeft');
|
||||||
await page.keyboard.press('Enter');
|
await expect(titleCell).toBeFocused();
|
||||||
await expect(titleEditor).toBeFocused();
|
|
||||||
|
await cueCell.click();
|
||||||
|
await expect(cueEditor).toBeFocused();
|
||||||
const cueBeforeCancel = await cueEditor.inputValue();
|
const cueBeforeCancel = await cueEditor.inputValue();
|
||||||
await cueEditor.click();
|
|
||||||
await cueEditor.fill(`${cueBeforeCancel} temporary`);
|
await cueEditor.fill(`${cueBeforeCancel} temporary`);
|
||||||
await cueEditor.press('Escape');
|
await cueEditor.press('Escape');
|
||||||
await expect(cueEditor).not.toBeFocused();
|
await expect(cueEditor).toHaveCount(0);
|
||||||
await expect(cueEditor).toHaveValue(cueBeforeCancel);
|
await expect(cueCell).toContainText(cueBeforeCancel);
|
||||||
|
await expect(cueCell).toBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cuesheet background edit from empty state', async ({ page }) => {
|
||||||
|
// create an empty rundown
|
||||||
|
await page.goto('/editor');
|
||||||
|
await page.getByRole('button', { name: 'Toggle settings' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Manage rundowns' }).click();
|
||||||
|
await page.getByRole('button', { name: 'New' }).nth(1).click();
|
||||||
|
const emptyName = `empty-${Date.now()}`;
|
||||||
|
await page.getByRole('textbox', { name: 'Rundown title' }).fill(emptyName);
|
||||||
|
await page.getByRole('button', { name: 'Create rundown' }).click();
|
||||||
|
|
||||||
|
// edit it in the cuesheet (scope to the rundown we just created so retries stay isolated)
|
||||||
|
await page.getByRole('row', { name: emptyName }).getByTestId('rundown_menu').click();
|
||||||
|
await page.getByText('Edit in cuesheet').click();
|
||||||
|
|
||||||
|
// expect to see and empty screen
|
||||||
|
await expect(page.getByRole('button', { name: 'Create Event' })).toBeVisible();
|
||||||
|
|
||||||
|
// create 1 event
|
||||||
|
await page.getByRole('button', { name: 'Create Event' }).click();
|
||||||
|
|
||||||
|
// and expect to find it
|
||||||
|
await expect(page.getByTestId('cuesheet-event')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -231,9 +231,10 @@ test.describe('Sharing from cuesheet', () => {
|
|||||||
|
|
||||||
// Verify that the title is visible and editable
|
// Verify that the title is visible and editable
|
||||||
await expect(page.getByTestId('cuesheet-event').getByRole('cell', { name: 'title' })).toBeVisible();
|
await expect(page.getByTestId('cuesheet-event').getByRole('cell', { name: 'title' })).toBeVisible();
|
||||||
const titleEditor = page.getByTestId('cuesheet-event').getByTestId('cuesheet-editor-title');
|
// the editor mounts on demand: clicking the cell opens it
|
||||||
await titleEditor.click();
|
const firstEvent = page.getByTestId('cuesheet-event').first();
|
||||||
await expect(titleEditor).toBeEditable();
|
await firstEvent.getByTestId('cuesheet-cell-title').click();
|
||||||
|
await expect(firstEvent.getByTestId('cuesheet-editor-title')).toBeEditable();
|
||||||
|
|
||||||
// other elements are not there
|
// other elements are not there
|
||||||
await expect(page.getByRole('cell', { name: 'Duration' })).toBeHidden();
|
await expect(page.getByRole('cell', { name: 'Duration' })).toBeHidden();
|
||||||
|
|||||||
Reference in New Issue
Block a user