From 261cd25bec9748d7d41c8cfb2b028ad44991db05 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sat, 10 Jan 2026 09:41:40 +0100 Subject: [PATCH] refactor: table mode and context --- .../common/context/EntryActionsContext.tsx | 24 ++++++ .../src/features/rundown/Rundown.module.scss | 1 - apps/client/src/features/rundown/Rundown.tsx | 4 +- .../rundown/RundownExport.module.scss | 8 +- .../src/features/rundown/RundownExport.tsx | 41 ++++++--- .../src/features/rundown/RundownList.tsx | 29 +++++++ .../src/features/rundown/RundownWrapper.tsx | 34 -------- .../features/rundown/common/TitleEditor.tsx | 4 +- .../rundown/context/RundownActionsContext.tsx | 24 ------ .../rundown/entry-editor/EventEditor.tsx | 4 +- .../rundown/entry-editor/GroupEditor.tsx | 4 +- .../rundown/entry-editor/MilestoneEditor.tsx | 4 +- .../composite/EventEditorTimes.tsx | 4 +- .../composite/EventEditorTitles.tsx | 4 +- .../composite/EventEditorTriggers.tsx | 6 +- .../QuickAddButtons.module.scss | 1 + .../quick-add-buttons/QuickAddButtons.tsx | 4 +- .../quick-add-cursor/QuickAddInline.tsx | 4 +- .../rundown-delay}/BlockRadio.module.scss | 0 .../rundown/rundown-delay}/BlockRadio.tsx | 0 .../rundown-delay}/DelayInput.module.scss | 0 .../rundown/rundown-delay}/DelayInput.tsx | 6 +- .../rundown/rundown-delay/RundownDelay.tsx | 6 +- .../rundown/rundown-event/RundownEvent.tsx | 84 +++++++------------ .../rundown-event/RundownEventInner.tsx | 16 +--- .../composite/RundownEventPlayback.tsx | 4 +- .../rundown/rundown-group/RundownGroup.tsx | 4 +- .../rundown-header/RundownHeader.module.scss | 42 ++++++++++ .../rundown/rundown-header/RundownHeader.tsx | 42 +--------- .../rundown/rundown-header/RundownMenu.tsx | 49 +++++++---- .../rundown-header/RundownSettings.tsx | 33 ++++++++ .../rundown-milestone/RundownMilestone.tsx | 4 +- .../rundown-table/EditorTableSettings.tsx | 3 - .../src/features/rundown/rundown.utils.ts | 39 +++++++++ .../src/features/rundown/rundownViewMode.ts | 5 ++ .../rundown/time-input-flow/TimeInputFlow.tsx | 4 +- .../src/features/rundown/useEventSelection.ts | 9 +- .../src/views/cuesheet/CuesheetPage.tsx | 7 +- .../cuesheet/cuesheet-table/CuesheetTable.tsx | 35 ++++++-- .../DelayRow.module.scss | 6 ++ .../cuesheet-table-elements/DelayRow.tsx | 11 ++- .../EventRow.module.scss | 6 ++ .../cuesheet-table-elements/EventRow.tsx | 3 + .../GroupRow.module.scss | 6 ++ .../cuesheet-table-elements/GroupRow.tsx | 3 + .../MilestoneRow.module.scss | 6 ++ .../cuesheet-table-elements/MilestoneRow.tsx | 3 + .../cuesheet-table-menu/CuesheetTableMenu.tsx | 4 +- .../cuesheet-table-menu/EditorTableMenu.tsx | 4 +- .../CuesheetTableSettings.tsx | 7 +- .../editor/EditorViewOptions.module.scss | 67 +++++++++++++++ .../src/views/editor/EditorViewOptions.tsx | 50 +++++++++++ .../src/views/editor/ProtectedEditor.tsx | 4 +- .../src/views/editor/finder/useFinder.tsx | 4 +- e2e/tests/000-upload-showfile.spec.ts | 3 +- e2e/tests/features/203-delay-block.spec.ts | 6 +- e2e/tests/features/204-editor-crud.spec.ts | 3 +- e2e/tests/features/205-operator.spec.ts | 3 +- e2e/tests/features/206-url-preset.spec.ts | 3 +- .../features/209-rundown-shortcuts.spec.ts | 18 ++-- e2e/tests/features/211-report-data.spec.ts | 6 +- e2e/tests/features/212-time-until.spec.ts | 2 + .../213-arrange-while-playing.spec.ts | 6 +- .../features/301-spreadsheet-import.spec.ts | 3 +- 64 files changed, 560 insertions(+), 273 deletions(-) create mode 100644 apps/client/src/common/context/EntryActionsContext.tsx create mode 100644 apps/client/src/features/rundown/RundownList.tsx delete mode 100644 apps/client/src/features/rundown/RundownWrapper.tsx delete mode 100644 apps/client/src/features/rundown/context/RundownActionsContext.tsx rename apps/client/src/{common/components/input/delay-input => features/rundown/rundown-delay}/BlockRadio.module.scss (100%) rename apps/client/src/{common/components/input/delay-input => features/rundown/rundown-delay}/BlockRadio.tsx (100%) rename apps/client/src/{common/components/input/delay-input => features/rundown/rundown-delay}/DelayInput.module.scss (100%) rename apps/client/src/{common/components/input/delay-input => features/rundown/rundown-delay}/DelayInput.tsx (94%) create mode 100644 apps/client/src/features/rundown/rundown-header/RundownSettings.tsx create mode 100644 apps/client/src/features/rundown/rundownViewMode.ts create mode 100644 apps/client/src/views/editor/EditorViewOptions.module.scss create mode 100644 apps/client/src/views/editor/EditorViewOptions.tsx diff --git a/apps/client/src/common/context/EntryActionsContext.tsx b/apps/client/src/common/context/EntryActionsContext.tsx new file mode 100644 index 000000000..7bb56d80f --- /dev/null +++ b/apps/client/src/common/context/EntryActionsContext.tsx @@ -0,0 +1,24 @@ +import { createContext, PropsWithChildren, useContext } from 'react'; + +import { useEntryActions } from '../hooks/useEntryAction'; + +type EntryActionsContextValue = ReturnType; +const EntryActionsContext = createContext(null); + +interface EntryActionsProviderProps extends PropsWithChildren { + actions: EntryActionsContextValue; +} + +export function EntryActionsProvider({ children, actions }: EntryActionsProviderProps) { + return {children}; +} + +export function useEntryActionsContext(): EntryActionsContextValue { + const context = useContext(EntryActionsContext); + + if (!context) { + throw new Error('useEntryActionsContext must be used within EntryActionsProvider'); + } + + return context; +} diff --git a/apps/client/src/features/rundown/Rundown.module.scss b/apps/client/src/features/rundown/Rundown.module.scss index b6499a527..743160330 100644 --- a/apps/client/src/features/rundown/Rundown.module.scss +++ b/apps/client/src/features/rundown/Rundown.module.scss @@ -5,7 +5,6 @@ .rundownContainer { margin-top: 1rem; - overflow-y: scroll; height: 100%; } diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index c2a3ae4f0..fd08c4958 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -38,7 +38,7 @@ import { useEntryCopy } from '../../common/stores/entryCopyStore'; import { lastMetadataKey, RundownMetadataObject } from '../../common/utils/rundownMetadata'; import { AppMode, sessionKeys } from '../../ontimeConfig'; -import { useRundownEntryActions } from './context/RundownActionsContext'; +import { useEntryActionsContext } from '../../common/context/EntryActionsContext'; import QuickAddButtons from './entry-editor/quick-add-buttons/QuickAddButtons'; import QuickAddInline from './entry-editor/quick-add-cursor/QuickAddInline'; import RundownGroup from './rundown-group/RundownGroup'; @@ -72,7 +72,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) { }); const collapsedGroupSet = useMemo(() => new Set(collapsedGroups), [collapsedGroups]); - const { addEntry, clone, deleteEntry, move, reorderEntry } = useRundownEntryActions(); + const { addEntry, clone, deleteEntry, move, reorderEntry } = useEntryActionsContext(); const setEntryCopyId = useEntryCopy((state) => state.setEntryCopyId); // cursor diff --git a/apps/client/src/features/rundown/RundownExport.module.scss b/apps/client/src/features/rundown/RundownExport.module.scss index a795aa146..c9fdaa3c0 100644 --- a/apps/client/src/features/rundown/RundownExport.module.scss +++ b/apps/client/src/features/rundown/RundownExport.module.scss @@ -1,6 +1,7 @@ .rundownExport { height: 100%; - flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */ + flex: 1 1 0; /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */ + min-width: 0; &.extracted { .list { @@ -20,6 +21,11 @@ height: 100%; } +.rundownRoot { + height: calc(100% - 1.5rem); + width: 100%; +} + .list { display: flex; height: inherit; diff --git a/apps/client/src/features/rundown/RundownExport.tsx b/apps/client/src/features/rundown/RundownExport.tsx index cc0c41dff..ecd075f9a 100644 --- a/apps/client/src/features/rundown/RundownExport.tsx +++ b/apps/client/src/features/rundown/RundownExport.tsx @@ -5,18 +5,23 @@ import * as Editor from '../../common/components/editor-utils/EditorUtils'; import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'; import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu'; import ProtectRoute from '../../common/components/protect-route/ProtectRoute'; +import { EntryActionsProvider } from '../../common/context/EntryActionsContext'; import { useEntryActions } from '../../common/hooks/useEntryAction'; import { useIsSmallDevice } from '../../common/hooks/useIsSmallDevice'; import { handleLinks } from '../../common/utils/linkUtils'; import { cx } from '../../common/utils/styleUtils'; import { getIsNavigationLocked } from '../../externals'; import { AppMode, sessionKeys } from '../../ontimeConfig'; +import EditorEditModal from '../../views/cuesheet/cuesheet-edit-modal/EditorEditModal'; -import { RundownActionsProvider } from './context/RundownActionsContext'; import RundownEntryEditor from './entry-editor/RundownEntryEditor'; import FinderPlacement from './placements/FinderPlacement'; import { RundownContextMenu } from './rundown-context-menu/RundownContextMenu'; -import RundownWrapper, { RundownViewMode } from './RundownWrapper'; +import RundownHeader from './rundown-header/RundownHeader'; +import RundownHeaderMobile from './rundown-header/RundownHeaderMobile'; +import RundownTable from './rundown-table/RundownTable'; +import RundownList from './RundownList'; +import { DEFAULT_RUNDOWN_VIEW_MODE, RUNDOWN_VIEW_MODE_STORAGE_KEY, RundownViewMode } from './rundownViewMode'; import style from './RundownExport.module.scss'; @@ -29,15 +34,15 @@ function RundownExport() { defaultValue: AppMode.Edit, }); const [viewMode, setViewMode] = useSessionStorage({ - key: 'rundown-view-mode', - defaultValue: 'list', + key: RUNDOWN_VIEW_MODE_STORAGE_KEY, + defaultValue: DEFAULT_RUNDOWN_VIEW_MODE, }); const isSmallDevice = useIsSmallDevice(); const entryActions = useEntryActions(); if (isSmallDevice && isExtracted) { return ( - +
- +
-
+ ); } const hideSideBar = (isExtracted && editorMode === 'run') || viewMode === 'table'; return ( - +
@@ -70,7 +75,7 @@ function RundownExport() { {!isExtracted && handleLinks('rundown', event)} />} - + @@ -84,6 +89,22 @@ function RundownExport() {
-
+ + ); +} + +interface RundownRootProps { + isSmallDevice?: boolean; + viewMode: RundownViewMode; + setViewMode: (mode: RundownViewMode) => void; +} + +function RundownRoot({ isSmallDevice, viewMode, setViewMode }: RundownRootProps) { + return ( +
+ {isSmallDevice ? : } + {viewMode === 'list' ? : } + +
); } diff --git a/apps/client/src/features/rundown/RundownList.tsx b/apps/client/src/features/rundown/RundownList.tsx new file mode 100644 index 000000000..46a0dfc3f --- /dev/null +++ b/apps/client/src/features/rundown/RundownList.tsx @@ -0,0 +1,29 @@ +import { memo } from 'react'; + +import Empty from '../../common/components/state/Empty'; +import { useRundownEditor } from '../../common/hooks/useSocket'; +import { useRundownWithMetadata } from '../../common/hooks-query/useRundown'; + +import Rundown from './Rundown'; + +export default memo(RundownList); +function RundownList() { + const { data, status, rundownMetadata } = useRundownWithMetadata(); + const featureData = useRundownEditor(); + + const isLoading = status !== 'success' || !data || !rundownMetadata; + + if (isLoading) { + return ; + } + + return ( + + ); +} diff --git a/apps/client/src/features/rundown/RundownWrapper.tsx b/apps/client/src/features/rundown/RundownWrapper.tsx deleted file mode 100644 index 028f5735b..000000000 --- a/apps/client/src/features/rundown/RundownWrapper.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import Empty from '../../common/components/state/Empty'; -import { useRundownWithMetadata } from '../../common/hooks-query/useRundown'; -import EditorEditModal from '../../views/cuesheet/cuesheet-edit-modal/EditorEditModal'; - -import RundownHeader from './rundown-header/RundownHeader'; -import RundownHeaderMobile from './rundown-header/RundownHeaderMobile'; -import RundownTable from './rundown-table/RundownTable'; -import Rundown from './Rundown'; - -import styles from './Rundown.module.scss'; - -export type RundownViewMode = 'list' | 'table'; - -interface RundownWrapperProps { - isSmallDevice?: boolean; - viewMode: RundownViewMode; - setViewMode: (mode: RundownViewMode) => void; -} - -export default function RundownWrapper({ isSmallDevice, viewMode, setViewMode }: RundownWrapperProps) { - const { data, status, rundownMetadata } = useRundownWithMetadata(); - - const isLoading = status !== 'success' || !data || !rundownMetadata; - - return ( -
- {isSmallDevice ? : } - {isLoading && } - {!isLoading && viewMode === 'list' && } - {!isLoading && viewMode === 'table' && } - -
- ); -} diff --git a/apps/client/src/features/rundown/common/TitleEditor.tsx b/apps/client/src/features/rundown/common/TitleEditor.tsx index 5199bd866..1758b5017 100644 --- a/apps/client/src/features/rundown/common/TitleEditor.tsx +++ b/apps/client/src/features/rundown/common/TitleEditor.tsx @@ -2,8 +2,8 @@ import { useCallback, useRef } from 'react'; import Input from '../../../common/components/input/input/Input'; import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import { cx } from '../../../common/utils/styleUtils'; -import { useRundownEntryActions } from '../context/RundownActionsContext'; import style from './TitleEditor.module.scss'; @@ -15,7 +15,7 @@ interface TitleEditorProps { } export default function TitleEditor({ title, entryId, placeholder, className }: TitleEditorProps) { - const { updateEntry } = useRundownEntryActions(); + const { updateEntry } = useEntryActionsContext(); const ref = useRef(null); const submitCallback = useCallback( (text: string) => { diff --git a/apps/client/src/features/rundown/context/RundownActionsContext.tsx b/apps/client/src/features/rundown/context/RundownActionsContext.tsx deleted file mode 100644 index 12aa75f84..000000000 --- a/apps/client/src/features/rundown/context/RundownActionsContext.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { createContext, PropsWithChildren, useContext } from 'react'; - -import { useEntryActions } from '../../../common/hooks/useEntryAction'; - -type EntryActionsContextValue = ReturnType; -const RundownActionsContext = createContext(null); - -interface RundownActionsProviderProps extends PropsWithChildren { - actions: EntryActionsContextValue; -} - -export function RundownActionsProvider({ children, actions }: RundownActionsProviderProps) { - return {children}; -} - -export function useRundownEntryActions(): EntryActionsContextValue { - const context = useContext(RundownActionsContext); - - if (!context) { - throw new Error('useRundownEntryActions must be used within RundownActionsProvider'); - } - - return context; -} diff --git a/apps/client/src/features/rundown/entry-editor/EventEditor.tsx b/apps/client/src/features/rundown/entry-editor/EventEditor.tsx index 2a2a472bb..0e544b55c 100644 --- a/apps/client/src/features/rundown/entry-editor/EventEditor.tsx +++ b/apps/client/src/features/rundown/entry-editor/EventEditor.tsx @@ -3,8 +3,8 @@ import { OntimeEvent } from 'ontime-types'; import * as Editor from '../../../common/components/editor-utils/EditorUtils'; import AppLink from '../../../common/components/link/app-link/AppLink'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import useCustomFields from '../../../common/hooks-query/useCustomFields'; -import { useRundownEntryActions } from '../context/RundownActionsContext'; import EntryEditorCustomFields from './composite/EventEditorCustomFields'; import EventEditorTimes from './composite/EventEditorTimes'; @@ -22,7 +22,7 @@ interface EventEditorProps { export default function EventEditor({ event }: EventEditorProps) { const { data: customFields } = useCustomFields(); - const { updateEntry } = useRundownEntryActions(); + const { updateEntry } = useEntryActionsContext(); const isEditor = window.location.pathname.includes('editor'); diff --git a/apps/client/src/features/rundown/entry-editor/GroupEditor.tsx b/apps/client/src/features/rundown/entry-editor/GroupEditor.tsx index 47d4d36d4..ba59fbd43 100644 --- a/apps/client/src/features/rundown/entry-editor/GroupEditor.tsx +++ b/apps/client/src/features/rundown/entry-editor/GroupEditor.tsx @@ -5,11 +5,11 @@ import { millisToString } from 'ontime-utils'; import * as Editor from '../../../common/components/editor-utils/EditorUtils'; import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect'; import AppLink from '../../../common/components/link/app-link/AppLink'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import useCustomFields from '../../../common/hooks-query/useCustomFields'; import { getOffsetState } from '../../../common/utils/offset'; import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils'; import TextLikeInput from '../../../views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput'; -import { useRundownEntryActions } from '../context/RundownActionsContext'; import EntryEditorCustomFields from './composite/EventEditorCustomFields'; import EventTextArea from './composite/EventTextArea'; @@ -28,7 +28,7 @@ interface GroupEditorProps { export default function GroupEditor({ group }: GroupEditorProps) { const { data: customFields } = useCustomFields(); - const { updateEntry } = useRundownEntryActions(); + const { updateEntry } = useEntryActionsContext(); const handleSubmit = useCallback( (field: GroupEditorUpdateTextFields | GroupEditorUpdateMaybeNumberFields, value: string | MaybeNumber) => { diff --git a/apps/client/src/features/rundown/entry-editor/MilestoneEditor.tsx b/apps/client/src/features/rundown/entry-editor/MilestoneEditor.tsx index 9eb03e61c..4a3f39030 100644 --- a/apps/client/src/features/rundown/entry-editor/MilestoneEditor.tsx +++ b/apps/client/src/features/rundown/entry-editor/MilestoneEditor.tsx @@ -5,8 +5,8 @@ import * as Editor from '../../../common/components/editor-utils/EditorUtils'; import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect'; import Input from '../../../common/components/input/input/Input'; import AppLink from '../../../common/components/link/app-link/AppLink'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import useCustomFields from '../../../common/hooks-query/useCustomFields'; -import { useRundownEntryActions } from '../context/RundownActionsContext'; import EntryEditorCustomFields from './composite/EventEditorCustomFields'; import EventTextArea from './composite/EventTextArea'; @@ -22,7 +22,7 @@ interface MilestoneEditorProps { } export default function MilestoneEditor({ milestone }: MilestoneEditorProps) { const { data: customFields } = useCustomFields(); - const { updateEntry } = useRundownEntryActions(); + const { updateEntry } = useEntryActionsContext(); const handleSubmit = useCallback( (field: MilestoneEditorUpdateTextFields, value: string) => { diff --git a/apps/client/src/features/rundown/entry-editor/composite/EventEditorTimes.tsx b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTimes.tsx index 89b975267..ef35126e8 100644 --- a/apps/client/src/features/rundown/entry-editor/composite/EventEditorTimes.tsx +++ b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTimes.tsx @@ -8,8 +8,8 @@ import TimeInput from '../../../../common/components/input/time-input/TimeInput' import Select from '../../../../common/components/select/Select'; import Switch from '../../../../common/components/switch/Switch'; import Tooltip from '../../../../common/components/tooltip/Tooltip'; +import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext'; import { millisToDelayString } from '../../../../common/utils/dateConfig'; -import { useRundownEntryActions } from '../../context/RundownActionsContext'; import TimeInputFlow from '../../time-input-flow/TimeInputFlow'; import style from '../EntryEditor.module.scss'; @@ -46,7 +46,7 @@ function EventEditorTimes({ timeWarning, timeDanger, }: EventEditorTimesProps) { - const { updateEntry } = useRundownEntryActions(); + const { updateEntry } = useEntryActionsContext(); const handleSubmit = (field: HandledActions, value: string | boolean) => { if (field === 'countToEnd') { diff --git a/apps/client/src/features/rundown/entry-editor/composite/EventEditorTitles.tsx b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTitles.tsx index 58507ccef..4cfaea9c3 100644 --- a/apps/client/src/features/rundown/entry-editor/composite/EventEditorTitles.tsx +++ b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTitles.tsx @@ -5,7 +5,7 @@ import * as Editor from '../../../../common/components/editor-utils/EditorUtils' import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect'; import Input from '../../../../common/components/input/input/Input'; import Switch from '../../../../common/components/switch/Switch'; -import { useRundownEntryActions } from '../../context/RundownActionsContext'; +import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext'; import EventTextArea from './EventTextArea'; import EntryEditorTextInput from './EventTextInput'; @@ -23,7 +23,7 @@ interface EventEditorTitlesProps { export default memo(EventEditorTitles); function EventEditorTitles({ eventId, cue, flag, title, note, colour }: EventEditorTitlesProps) { - const { updateEntry } = useRundownEntryActions(); + const { updateEntry } = useEntryActionsContext(); const cueSubmitHandler = (_field: string, newValue: string) => { updateEntry({ id: eventId, cue: sanitiseCue(newValue) }); diff --git a/apps/client/src/features/rundown/entry-editor/composite/EventEditorTriggers.tsx b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTriggers.tsx index e7d815622..4b1d24876 100644 --- a/apps/client/src/features/rundown/entry-editor/composite/EventEditorTriggers.tsx +++ b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTriggers.tsx @@ -8,8 +8,8 @@ import IconButton from '../../../../common/components/buttons/IconButton'; import Select from '../../../../common/components/select/Select'; import Tag from '../../../../common/components/tag/Tag'; import Tooltip from '../../../../common/components/tooltip/Tooltip'; +import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext'; import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings'; -import { useRundownEntryActions } from '../../context/RundownActionsContext'; import { eventTriggerOptions } from './eventTrigger.constants'; @@ -38,7 +38,7 @@ interface EventTriggerFormProps { function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) { const { data: automationSettings } = useAutomationSettings(); - const { updateEntry } = useRundownEntryActions(); + const { updateEntry } = useEntryActionsContext(); const [automationId, setAutomationId] = useState(undefined); const [cycleValue, setCycleValue] = useState(TimerLifeCycle.onStart); @@ -123,7 +123,7 @@ interface ExistingEventTriggersProps { } function ExistingEventTriggers({ eventId, triggers }: ExistingEventTriggersProps) { - const { updateEntry } = useRundownEntryActions(); + const { updateEntry } = useEntryActionsContext(); const { data: automationSettings } = useAutomationSettings(); const handleDelete = useCallback( diff --git a/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.module.scss b/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.module.scss index f1175abe5..3e0d137a8 100644 --- a/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.module.scss +++ b/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.module.scss @@ -2,6 +2,7 @@ display: flex; align-items: center; gap: 1rem; + padding-left: 0.5rem; padding-block: 0.5rem; background: color-mix(in srgb, transparent 90%, var(--user-bg, transparent) 10%); diff --git a/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.tsx b/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.tsx index dee4684b3..a7b6a32c7 100644 --- a/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.tsx +++ b/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.tsx @@ -4,8 +4,8 @@ import { Toolbar } from '@base-ui/react/toolbar'; import { MaybeString, SupportedEntry } from 'ontime-types'; import Button from '../../../../common/components/buttons/Button'; +import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext'; import { cx } from '../../../../common/utils/styleUtils'; -import { useRundownEntryActions } from '../../context/RundownActionsContext'; import style from './QuickAddButtons.module.scss'; @@ -17,7 +17,7 @@ interface QuickAddButtonsProps { export default memo(QuickAddButtons); function QuickAddButtons({ previousEventId, parentGroup, backgroundColor }: QuickAddButtonsProps) { - const { addEntry } = useRundownEntryActions(); + const { addEntry } = useEntryActionsContext(); const addEvent = () => { addEntry( diff --git a/apps/client/src/features/rundown/entry-editor/quick-add-cursor/QuickAddInline.tsx b/apps/client/src/features/rundown/entry-editor/quick-add-cursor/QuickAddInline.tsx index 712082b2c..95035f631 100644 --- a/apps/client/src/features/rundown/entry-editor/quick-add-cursor/QuickAddInline.tsx +++ b/apps/client/src/features/rundown/entry-editor/quick-add-cursor/QuickAddInline.tsx @@ -4,7 +4,7 @@ import { MaybeString, SupportedEntry } from 'ontime-types'; import IconButton from '../../../../common/components/buttons/IconButton'; import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu'; -import { useRundownEntryActions } from '../../context/RundownActionsContext'; +import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext'; import style from './QuickAddInline.module.scss'; @@ -16,7 +16,7 @@ interface QuickAddInlineProps { export default memo(QuickAddInline); function QuickAddInline({ referenceEntryId, parentGroup, placement }: QuickAddInlineProps) { - const { addEntry } = useRundownEntryActions(); + const { addEntry } = useEntryActionsContext(); const handleAddEntry = (type: SupportedEntry) => { if (placement === 'before') { diff --git a/apps/client/src/common/components/input/delay-input/BlockRadio.module.scss b/apps/client/src/features/rundown/rundown-delay/BlockRadio.module.scss similarity index 100% rename from apps/client/src/common/components/input/delay-input/BlockRadio.module.scss rename to apps/client/src/features/rundown/rundown-delay/BlockRadio.module.scss diff --git a/apps/client/src/common/components/input/delay-input/BlockRadio.tsx b/apps/client/src/features/rundown/rundown-delay/BlockRadio.tsx similarity index 100% rename from apps/client/src/common/components/input/delay-input/BlockRadio.tsx rename to apps/client/src/features/rundown/rundown-delay/BlockRadio.tsx diff --git a/apps/client/src/common/components/input/delay-input/DelayInput.module.scss b/apps/client/src/features/rundown/rundown-delay/DelayInput.module.scss similarity index 100% rename from apps/client/src/common/components/input/delay-input/DelayInput.module.scss rename to apps/client/src/features/rundown/rundown-delay/DelayInput.module.scss diff --git a/apps/client/src/common/components/input/delay-input/DelayInput.tsx b/apps/client/src/features/rundown/rundown-delay/DelayInput.tsx similarity index 94% rename from apps/client/src/common/components/input/delay-input/DelayInput.tsx rename to apps/client/src/features/rundown/rundown-delay/DelayInput.tsx index 7b8e699ab..2fed3ac24 100644 --- a/apps/client/src/common/components/input/delay-input/DelayInput.tsx +++ b/apps/client/src/features/rundown/rundown-delay/DelayInput.tsx @@ -1,8 +1,8 @@ import { KeyboardEvent, useEffect, useRef, useState } from 'react'; import { millisToString, parseUserTime } from 'ontime-utils'; -import { useRundownEntryActions } from '../../../../features/rundown/context/RundownActionsContext'; -import Input from '../input/Input'; +import Input from '../../../common/components/input/input/Input'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import BlockRadio from './BlockRadio'; @@ -14,7 +14,7 @@ interface DelayInputProps { } export default function DelayInput({ eventId, duration }: DelayInputProps) { - const { updateEntry } = useRundownEntryActions(); + const { updateEntry } = useEntryActionsContext(); const [value, setValue] = useState(''); const inputRef = useRef(null); diff --git a/apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx b/apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx index 9b020afa5..b3bc8f67d 100644 --- a/apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx +++ b/apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx @@ -5,9 +5,9 @@ import { CSS } from '@dnd-kit/utilities'; import { OntimeDelay } from 'ontime-types'; import Button from '../../../common/components/buttons/Button'; -import DelayInput from '../../../common/components/input/delay-input/DelayInput'; +import DelayInput from './DelayInput'; import { cx } from '../../../common/utils/styleUtils'; -import { useRundownEntryActions } from '../context/RundownActionsContext'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import style from './RundownDelay.module.scss'; @@ -19,7 +19,7 @@ interface RundownDelayProps { export default function RundownDelay({ data, hasCursor }: RundownDelayProps) { 'use memo'; - const { applyDelay, deleteEntry } = useRundownEntryActions(); + const { applyDelay, deleteEntry } = useEntryActionsContext(); const handleRef = useRef(null); const { diff --git a/apps/client/src/features/rundown/rundown-event/RundownEvent.tsx b/apps/client/src/features/rundown/rundown-event/RundownEvent.tsx index a7564d7c9..d35737e86 100644 --- a/apps/client/src/features/rundown/rundown-event/RundownEvent.tsx +++ b/apps/client/src/features/rundown/rundown-event/RundownEvent.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { MouseEvent, useEffect, useRef } from 'react'; import { IoAdd, IoDuplicateOutline, @@ -15,9 +15,9 @@ import { CSS } from '@dnd-kit/utilities'; import { EndAction, EntryId, Playback, TimerType, TimeStrategy } from 'ontime-types'; import { isPlaybackActive } from 'ontime-utils'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import { useContextMenu } from '../../../common/hooks/useContextMenu'; import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; -import { useRundownEntryActions } from '../context/RundownActionsContext'; import { useEventIdSwapping } from '../useEventIdSwapping'; import { getSelectionMode, useEventSelection } from '../useEventSelection'; @@ -97,7 +97,7 @@ export default function RundownEvent({ const setSelectedEventId = useEventIdSwapping((state) => state.setSelectedEventId); const clearSelectedEventId = useEventIdSwapping((state) => state.clearSelectedEventId); - const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useRundownEntryActions(); + const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useEntryActionsContext(); const isSelected = useEventSelection((state) => state.selectedEvents.has(eventId)); const unselect = useEventSelection((state) => state.unselect); @@ -107,7 +107,6 @@ export default function RundownEvent({ const selectedEvents = useEventSelection((state) => state.selectedEvents); const handleRef = useRef(null); - const [isVisible, setIsVisible] = useState(false); const [onContextMenu] = useContextMenu(() => selectedEvents.size > 1 @@ -236,31 +235,6 @@ export default function RundownEvent({ } }, [hasCursor]); - useLayoutEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting) { - setIsVisible(true); - } - }, - { - root: null, - threshold: 1, - }, - ); - - const handleRefCurrent = handleRef.current; - if (handleRefCurrent) { - observer.observe(handleRefCurrent); - } - - return () => { - if (handleRefCurrent) { - observer.unobserve(handleRefCurrent); - } - }; - }, [handleRef]); - const blockClasses = cx([ style.rundownEvent, skip ? style.skip : null, @@ -308,33 +282,31 @@ export default function RundownEvent({ {cue} - {isVisible && ( - - )} + ); } diff --git a/apps/client/src/features/rundown/rundown-event/RundownEventInner.tsx b/apps/client/src/features/rundown/rundown-event/RundownEventInner.tsx index b85089092..e387afa7d 100644 --- a/apps/client/src/features/rundown/rundown-event/RundownEventInner.tsx +++ b/apps/client/src/features/rundown/rundown-event/RundownEventInner.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useState } from 'react'; +import { memo } from 'react'; import { IoArrowDown, IoArrowUp, @@ -76,17 +76,11 @@ function RundownEventInner({ isLinkedToLoaded, hasTriggers, }: RundownEventInnerProps) { - const [renderInner, setRenderInner] = useState(false); - const [editorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit, }); - useEffect(() => { - setRenderInner(true); - }, []); - const eventIsPlaying = playback === Playback.Play; const eventIsPaused = playback === Playback.Pause; @@ -97,7 +91,7 @@ function RundownEventInner({ playBtnStyles._hover = {}; } - return !renderInner ? null : ( + return ( <>
; } -function TimerIcon(props: { type: TimerType; className: string }) { - const { type, className } = props; +function TimerIcon({ type, className }: { type: TimerType; className: string }) { if (type === TimerType.CountUp) { return ; } diff --git a/apps/client/src/features/rundown/rundown-event/composite/RundownEventPlayback.tsx b/apps/client/src/features/rundown/rundown-event/composite/RundownEventPlayback.tsx index fa8ebac1c..6a3aebe5a 100644 --- a/apps/client/src/features/rundown/rundown-event/composite/RundownEventPlayback.tsx +++ b/apps/client/src/features/rundown/rundown-event/composite/RundownEventPlayback.tsx @@ -3,8 +3,8 @@ import { IoPause, IoPlay, IoReload, IoRemoveCircle, IoRemoveCircleOutline } from import IconButton from '../../../../common/components/buttons/IconButton'; import Tooltip from '../../../../common/components/tooltip/Tooltip'; +import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext'; import { setEventPlayback } from '../../../../common/hooks/useSocket'; -import { useRundownEntryActions } from '../../context/RundownActionsContext'; import style from '../RundownEvent.module.scss'; @@ -26,7 +26,7 @@ function RundownEventPlayback({ loaded, disablePlayback, }: RundownEventPlaybackProps) { - const { updateEntry } = useRundownEntryActions(); + const { updateEntry } = useEntryActionsContext(); const toggleSkip = (event: MouseEvent) => { event.stopPropagation(); diff --git a/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx b/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx index de25b8f1c..e272a0320 100644 --- a/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx +++ b/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx @@ -18,7 +18,7 @@ import { getOffsetState } from '../../../common/utils/offset'; import { cx, getAccessibleColour, timerPlaceholder } from '../../../common/utils/styleUtils'; import { formatDuration } from '../../../common/utils/time'; import TitleEditor from '../common/TitleEditor'; -import { useRundownEntryActions } from '../context/RundownActionsContext'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import { canDrop } from '../rundown.utils'; import { useEventSelection } from '../useEventSelection'; @@ -36,7 +36,7 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }: 'use memo'; const handleRef = useRef(null); - const { clone, ungroup, deleteEntry } = useRundownEntryActions(); + const { clone, ungroup, deleteEntry } = useEntryActionsContext(); const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection); const selectedEvents = useEventSelection((state) => state.selectedEvents); diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss b/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss index b16f67f2d..dc1b6fbc2 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss +++ b/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss @@ -64,3 +64,45 @@ .separator { margin-inline: 1rem; } + +.column { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-self: start; +} + +.sectionTitle { + text-transform: uppercase; + font-weight: 600; + font-size: 0.75rem; + color: $gray-400; // Guessing color based on cuesheet +} + +.popoverContent { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + min-width: 200px; +} + +.column { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-self: start; +} + +.sectionTitle { + text-transform: uppercase; + font-weight: 600; + font-size: 0.75rem; + color: $gray-400; // Guessing color +} + +.popoverContent { + display: flex; + flex-direction: column; + gap: 1rem; +} diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx index f04325f33..8fd762f65 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx +++ b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx @@ -3,18 +3,16 @@ import { Toggle } from '@base-ui/react/toggle'; import { ToggleGroup } from '@base-ui/react/toggle-group'; import { Toolbar } from '@base-ui/react/toolbar'; import { useSessionStorage } from '@mantine/hooks'; -import { OffsetMode } from 'ontime-types'; import * as Editor from '../../../common/components/editor-utils/EditorUtils'; -import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket'; import { AppMode, sessionKeys } from '../../../ontimeConfig'; +import { RundownViewMode } from '../rundownViewMode'; import RundownMenu from './RundownMenu'; +import RundownSettings from './RundownSettings'; import style from './RundownHeader.module.scss'; -type RundownViewMode = 'list' | 'table'; - interface RundownHeaderProps { viewMode: RundownViewMode; setViewMode: (mode: RundownViewMode) => void; @@ -24,8 +22,6 @@ export default memo(RundownHeader); function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) { const [editorMode, setEditorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit }); - const offsetMode = useOffsetMode(); - const toggleAppMode = (mode: AppMode[]) => { // we need to stop user from deselecting a mode const newValue = mode.at(0); @@ -33,20 +29,6 @@ function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) { setEditorMode(newValue); }; - const toggleOffsetMode = (mode: OffsetMode[]) => { - // we need to stop user from deselecting a mode - const newValue = mode.at(0); - if (!newValue) return; - setOffsetMode(newValue); - }; - - const toggleViewMode = (mode: RundownViewMode[]) => { - // we need to stop user from deselecting a mode - const newValue = mode.at(0); - if (!newValue) return; - setViewMode(newValue); - }; - return ( @@ -60,25 +42,7 @@ function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) { - - } value={OffsetMode.Absolute} className={style.radioButton}> - Absolute - - } value={OffsetMode.Relative} className={style.radioButton}> - Relative - - - - - - - } value='list' className={style.radioButton}> - List - - } value='table' className={style.radioButton}> - Table - - + diff --git a/apps/client/src/features/rundown/rundown-header/RundownMenu.tsx b/apps/client/src/features/rundown/rundown-header/RundownMenu.tsx index 8aef5e700..211a839d6 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownMenu.tsx +++ b/apps/client/src/features/rundown/rundown-header/RundownMenu.tsx @@ -1,12 +1,14 @@ import { memo, useCallback } from 'react'; -import { IoTrash } from 'react-icons/io5'; +import { IoEllipsisHorizontal, IoList, IoTrash } from 'react-icons/io5'; import { Toolbar } from '@base-ui/react/toolbar'; -import { useDisclosure, useSessionStorage } from '@mantine/hooks'; +import { useDisclosure } from '@mantine/hooks'; import Button from '../../../common/components/buttons/Button'; +import IconButton from '../../../common/components/buttons/IconButton'; import Dialog from '../../../common/components/dialog/Dialog'; -import { AppMode, sessionKeys } from '../../../ontimeConfig'; -import { useRundownEntryActions } from '../context/RundownActionsContext'; +import { DropdownMenu } from '../../../common/components/dropdown-menu/DropdownMenu'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; +import useAppSettingsNavigation from '../../app-settings/useAppSettingsNavigation'; import { useEventSelection } from '../useEventSelection'; import style from './RundownHeader.module.scss'; @@ -16,11 +18,8 @@ function RundownMenu() { const [isOpen, handlers] = useDisclosure(); const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents); - const [editorMode] = useSessionStorage({ - key: sessionKeys.editorMode, - defaultValue: AppMode.Edit, - }); - const { deleteAllEntries } = useRundownEntryActions(); + const { deleteAllEntries } = useEntryActionsContext(); + const { setLocation } = useAppSettingsNavigation(); const deleteAll = useCallback(() => { deleteAllEntries(); @@ -30,15 +29,29 @@ function RundownMenu() { return ( <> - } - onClick={handlers.open} - disabled={editorMode === AppMode.Run} - className={style.apart} - > - - Clear all - +
+ } />} + items={[ + { + type: 'item', + label: 'Manage Rundowns...', + icon: IoList, + onClick: () => setLocation('manage'), + }, + { type: 'divider' }, + { + type: 'destructive', + label: 'Clear all', + icon: IoTrash, + onClick: handlers.open, + }, + ]} + > + + +
+ void; +} + +export default memo(RundownSettings); +function RundownSettings({ viewMode, setViewMode }: RundownSettingsProps) { + const toggleViewMode = (mode: RundownViewMode[]) => { + const newValue = mode.at(0); + if (!newValue) return; + setViewMode(newValue); + }; + + return ( + + } value='list' className={style.radioButton}> + List + + } value='table' className={style.radioButton}> + Table + + + ); +} diff --git a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx index 2edda1f78..8c3969014 100644 --- a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx +++ b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx @@ -8,7 +8,7 @@ import Input from '../../../common/components/input/input/Input'; import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput'; import { useContextMenu } from '../../../common/hooks/useContextMenu'; import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; -import { useRundownEntryActions } from '../context/RundownActionsContext'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import { useEventSelection } from '../useEventSelection'; import style from './RundownMilestone.module.scss'; @@ -25,7 +25,7 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl 'use memo'; const handleRef = useRef(null); - const { updateEntry, deleteEntry } = useRundownEntryActions(); + const { updateEntry, deleteEntry } = useEntryActionsContext(); const selectedEvents = useEventSelection((state) => state.selectedEvents); const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection); diff --git a/apps/client/src/features/rundown/rundown-table/EditorTableSettings.tsx b/apps/client/src/features/rundown/rundown-table/EditorTableSettings.tsx index d6117c0a2..9a9d3768b 100644 --- a/apps/client/src/features/rundown/rundown-table/EditorTableSettings.tsx +++ b/apps/client/src/features/rundown/rundown-table/EditorTableSettings.tsx @@ -6,7 +6,6 @@ import { ColumnSettings, ViewSettings, } from '../../../views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings'; - import { usePersistedRundownOptions } from '../rundown.options'; import style from '../../../views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.module.scss'; @@ -35,8 +34,6 @@ export default function EditorTableSettings({ handleResetReordering={handleResetReordering} handleClearToggles={handleClearToggles} /> - {/* No mode toggle - already in RundownHeader */} - {/* No share modal - cuesheet only */} ); } diff --git a/apps/client/src/features/rundown/rundown.utils.ts b/apps/client/src/features/rundown/rundown.utils.ts index 43a00675b..79eb58dbc 100644 --- a/apps/client/src/features/rundown/rundown.utils.ts +++ b/apps/client/src/features/rundown/rundown.utils.ts @@ -5,6 +5,9 @@ import { EntryId, isOntimeEvent, isOntimeGroup, RundownEntries, SupportedEntry } * ------------------------------------ * Due to limitations in dnd-kit we need to flatten the list of entries * This list should also be aware of any elements that are sortable (ie: group ends) + * + * Note: This creates the FULL structure including all entries and pseudo end-group entries. + * For rendering, use filterVisibleEntries() to exclude collapsed items. */ export function makeSortableList(order: EntryId[], entries: RundownEntries): EntryId[] { const flatIds: EntryId[] = []; @@ -31,6 +34,42 @@ export function makeSortableList(order: EntryId[], entries: RundownEntries): Ent return flatIds; } +/** + * Filters sortable list to only include visible entries based on collapsed state + * ------------------------------------ + * Excludes: + * - Children of collapsed groups + * - End-group markers of collapsed groups + * + * This is used by Virtuoso for rendering, while DND-kit uses the full sortableData. + */ +export function filterVisibleEntries( + sortableData: EntryId[], + entries: RundownEntries, + getIsCollapsed: (groupId: EntryId) => boolean, +): EntryId[] { + return sortableData.filter((entryId) => { + // group end pseudo entries are only shown if the group is expanded + if (entryId.startsWith('end-')) { + const parentId = entryId.split('end-')[1]; + return !getIsCollapsed(parentId); + } + + // retrieve the entry as usual + const entry = entries[entryId]; + if (!entry) { + return false; + } + + // if entry has a parent and parent is collapsed, filter it out + if (entry.type !== SupportedEntry.Group && 'parent' in entry && entry.parent) { + return !getIsCollapsed(entry.parent); + } + + return true; + }); +} + /** * Checks whether a drop operation is valid * Currently only used for validating dropping groups diff --git a/apps/client/src/features/rundown/rundownViewMode.ts b/apps/client/src/features/rundown/rundownViewMode.ts new file mode 100644 index 000000000..7bd7e6929 --- /dev/null +++ b/apps/client/src/features/rundown/rundownViewMode.ts @@ -0,0 +1,5 @@ +export const RUNDOWN_VIEW_MODES = ['list', 'table'] as const; +export type RundownViewMode = (typeof RUNDOWN_VIEW_MODES)[number]; + +export const DEFAULT_RUNDOWN_VIEW_MODE: RundownViewMode = 'list'; +export const RUNDOWN_VIEW_MODE_STORAGE_KEY = 'rundown-view-mode'; diff --git a/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx index e9593ff47..ecd3a11f5 100644 --- a/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx +++ b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx @@ -7,7 +7,7 @@ import IconButton from '../../../common/components/buttons/IconButton'; import * as Editor from '../../../common/components/editor-utils/EditorUtils'; import TimeInput from '../../../common/components/input/time-input/TimeInput'; import Tooltip from '../../../common/components/tooltip/Tooltip'; -import { useRundownEntryActions } from '../context/RundownActionsContext'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import TimeInputGroup from './TimeInputGroup'; @@ -37,7 +37,7 @@ function TimeInputFlow({ delay, showLabels, }: TimeInputFlowProps) { - const { updateEntry, updateTimer } = useRundownEntryActions(); + const { updateEntry, updateTimer } = useEntryActionsContext(); // In sync with EventEditorTimes const handleSubmit = (field: TimeField, value: string) => { diff --git a/apps/client/src/features/rundown/useEventSelection.ts b/apps/client/src/features/rundown/useEventSelection.ts index bd4dfb247..d1c06ad90 100644 --- a/apps/client/src/features/rundown/useEventSelection.ts +++ b/apps/client/src/features/rundown/useEventSelection.ts @@ -12,9 +12,12 @@ interface EventSelectionStore { selectedEvents: Set; anchoredIndex: MaybeNumber; cursor: MaybeString; + scrollTargetId: MaybeString; entryMode: 'event' | 'single' | null; setSingleEntrySelection: (selectionArgs: { id: EntryId }) => void; setSelectedEvents: (selectionArgs: { id: EntryId; index: number; selectMode: SelectionMode }) => void; + setScrollTargetId: (id: EntryId | null) => void; + clearScrollTargetId: () => void; clearSelectedEvents: () => void; clearMultiSelect: () => void; unselect: (id: EntryId) => void; @@ -24,6 +27,7 @@ export const useEventSelection = create()((set, get) => ({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, + scrollTargetId: null, entryMode: null, setSingleEntrySelection: ({ id }) => { set({ selectedEvents: new Set([id]), anchoredIndex: null, cursor: id, entryMode: 'single' }); @@ -99,7 +103,10 @@ export const useEventSelection = create()((set, get) => ({ }); } }, - clearSelectedEvents: () => set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null }), + setScrollTargetId: (id) => set({ scrollTargetId: id }), + clearScrollTargetId: () => set({ scrollTargetId: null }), + clearSelectedEvents: () => + set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null, scrollTargetId: null }), clearMultiSelect: () => { const { selectedEvents } = get(); const [firstSelected] = selectedEvents; diff --git a/apps/client/src/views/cuesheet/CuesheetPage.tsx b/apps/client/src/views/cuesheet/CuesheetPage.tsx index 8d56b02f5..eb4a43999 100644 --- a/apps/client/src/views/cuesheet/CuesheetPage.tsx +++ b/apps/client/src/views/cuesheet/CuesheetPage.tsx @@ -3,6 +3,8 @@ import { useDisclosure } from '@mantine/hooks'; import IconButton from '../../common/components/buttons/IconButton'; import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu'; +import { EntryActionsProvider } from '../../common/context/EntryActionsContext'; +import { useEntryActions } from '../../common/hooks/useEntryAction'; import { useWindowTitle } from '../../common/hooks/useWindowTitle'; import { getIsNavigationLocked } from '../../externals'; import CuesheetOverview from '../../features/overview/CuesheetOverview'; @@ -15,13 +17,14 @@ import styles from './CuesheetPage.module.scss'; export default function CuesheetPage() { const [isMenuOpen, menuHandler] = useDisclosure(); + const entryActions = useEntryActions(); useWindowTitle('Cuesheet'); const isLocked = getIsNavigationLocked(); return ( - <> +
@@ -35,6 +38,6 @@ export default function CuesheetPage() {
- +
); } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx index a63392fe4..2bdb3b9a0 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx @@ -6,12 +6,13 @@ import { isOntimeDelay, isOntimeGroup, isOntimeMilestone, OntimeEntry, TimeField import EmptyPage from '../../../common/components/state/EmptyPage'; import EmptyTableBody from '../../../common/components/state/EmptyTableBody'; -import { useEntryActions } from '../../../common/hooks/useEntryAction'; +import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import { useSelectedEventId } from '../../../common/hooks/useSocket'; import { useFlatRundownWithMetadata } from '../../../common/hooks-query/useRundown'; import type { ExtendedEntry } from '../../../common/utils/rundownMetadata'; import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options'; import EditorTableSettings from '../../../features/rundown/rundown-table/EditorTableSettings'; +import { useEventSelection } from '../../../features/rundown/useEventSelection'; import { AppMode } from '../../../ontimeConfig'; import { usePersistedCuesheetOptions } from '../cuesheet.options'; @@ -35,7 +36,7 @@ interface CuesheetTableProps { export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cuesheet' }: CuesheetTableProps) { const { data, status } = useFlatRundownWithMetadata(); - const { updateEntry, updateTimer } = useEntryActions(); + const { updateEntry, updateTimer } = useEntryActionsContext(); const useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions; const showDelayedTimes = useOptions((state) => state.showDelayedTimes); @@ -43,6 +44,7 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues const hideIndexColumn = useOptions((state) => state.hideIndexColumn); const selectedEventId = useSelectedEventId(); + const cursor = useEventSelection((state) => state.cursor); const virtuosoRef = useRef(null); const { listeners } = useTableNav(); @@ -112,15 +114,22 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues setColumnSizing({}); }, [setColumnSizing]); - // in run mode, we follow the selected row + // in edit mode, we follow the cursor (e.g. from finder) useEffect(() => { - if (cuesheetMode === AppMode.Edit || virtuosoRef.current === null || !selectedEventId) { + if (virtuosoRef.current === null) { return; } - const eventIndex = data.findIndex((event) => event.id === selectedEventId); - virtuosoRef.current.scrollToIndex({ index: eventIndex, behavior: 'smooth' }); - }, [cuesheetMode, data, selectedEventId]); + const targetId = cuesheetMode === AppMode.Edit ? cursor : selectedEventId; + if (!targetId) { + return; + } + + const eventIndex = data.findIndex((event) => event.id === targetId); + if (eventIndex !== -1) { + virtuosoRef.current.scrollToIndex({ index: eventIndex, behavior: 'smooth', align: 'center' }); + } + }, [cuesheetMode, data, selectedEventId, cursor]); /** * To improve performance on resizing, we memoise the column sizes @@ -184,6 +193,7 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues const row = rows[rowIndex]; const key = row.original.id; const entry = row.original; + const hasCursor = entry.id === cursor; if (isOntimeGroup(entry)) { return ( @@ -195,6 +205,7 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues rowIndex={row.index} table={table} injectedStyles={injectedStyles} + hasCursor={hasCursor} {...virtuosoProps} /> ); @@ -202,7 +213,13 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues if (isOntimeDelay(entry)) { return ( - + ); } @@ -219,6 +236,7 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues rowIndex={rowIndex} table={table} injectedStyles={injectedStyles} + hasCursor={hasCursor} {...virtuosoProps} /> ); @@ -241,6 +259,7 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues rowIndex={rowIndex} table={table} injectedStyles={injectedStyles} + hasCursor={hasCursor} {...virtuosoProps} /> ); diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.module.scss index 0a721e946..72ba1e049 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.module.scss @@ -5,6 +5,12 @@ color: $ontime-delay-text; border-left: 4px solid transparent; + &[data-cursor='true'] { + outline: 2px solid $blue-500; + outline-offset: -2px; + background: color-mix(in srgb, transparent 80%, var(--user-bg, $gray-500) 20%); + } + td { width: calc(100% - 4px); padding-block: 0.5rem; diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.tsx index 26de665f8..9b9ba3e84 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/DelayRow.tsx @@ -8,9 +8,10 @@ import style from './DelayRow.module.scss'; interface DelayRowProps { duration: number; injectedStyles?: CSSProperties; + hasCursor?: boolean; } -function DelayRow({ duration, injectedStyles, ...virtuosoProps }: DelayRowProps) { +function DelayRow({ duration, injectedStyles, hasCursor, ...virtuosoProps }: DelayRowProps) { const hideDelays = usePersistedCuesheetOptions((state) => state.hideDelays); if (hideDelays || duration === 0) { @@ -29,7 +30,13 @@ function DelayRow({ duration, injectedStyles, ...virtuosoProps }: DelayRowProps) const delayTime = millisToDelayString(duration, 'expanded'); return ( - + {delayTime} ); diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.module.scss index ea2fc8382..1434102cd 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.module.scss @@ -12,6 +12,12 @@ background: color-mix(in srgb, transparent 80%, var(--user-bg, $gray-500) 20%); } + &[data-cursor='true'] { + outline: 2px solid $blue-500; + outline-offset: -2px; + background: color-mix(in srgb, transparent 80%, var(--user-bg, $gray-500) 20%); + } + &.firstAfterGroup { margin-top: 2rem; } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx index 75f2cc0b9..794be0fb8 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx @@ -27,6 +27,7 @@ interface EventRowProps { rowIndex: number; table: Table>; injectedStyles?: CSSProperties; + hasCursor?: boolean; } export default function EventRow({ @@ -44,6 +45,7 @@ export default function EventRow({ rowIndex, table, injectedStyles, + hasCursor, ...virtuosoProps }: EventRowProps) { const { cuesheetMode, hideIndexColumn } = table.options.meta?.options ?? { @@ -87,6 +89,7 @@ export default function EventRow({ opacity: `${isPast ? '0.2' : '1'}`, '--user-bg': groupColour ?? 'transparent', }} + data-cursor={hasCursor} data-testid='cuesheet-event' {...virtuosoProps} > diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.module.scss index d58e9a38b..89824b1ac 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.module.scss @@ -17,6 +17,12 @@ font-weight: bold; + &[data-cursor='true'] { + outline: 2px solid $blue-500; + outline-offset: -2px; + background: color-mix(in srgb, transparent 80%, var(--user-bg, $gray-500) 20%); + } + td { min-height: 3.5rem; padding-top: 0.75rem !important; // fighting styles from cuesheet-table diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.tsx index dd03253b6..c08707def 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/GroupRow.tsx @@ -17,6 +17,7 @@ interface GroupRowProps { rowIndex: number; table: Table; injectedStyles?: CSSProperties; + hasCursor?: boolean; } export default function GroupRow({ @@ -26,6 +27,7 @@ export default function GroupRow({ rowIndex, table, injectedStyles, + hasCursor, ...virtuosoProps }: GroupRowProps) { const { cuesheetMode, hideIndexColumn } = table.options.meta?.options ?? { @@ -40,6 +42,7 @@ export default function GroupRow({ className={style.groupRow} style={{ ...injectedStyles, '--user-bg': colour }} data-testid='cuesheet-group' + data-cursor={hasCursor} {...virtuosoProps} > {cuesheetMode === AppMode.Edit && ( diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.module.scss index 03fc85917..cc0ec2e4c 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.module.scss @@ -13,6 +13,12 @@ background: color-mix(in srgb, transparent 80%, var(--user-bg, $gray-500) 20%); } + &[data-cursor='true'] { + outline: 2px solid $blue-500; + outline-offset: -2px; + background: color-mix(in srgb, transparent 80%, var(--user-bg, $gray-500) 20%); + } + td { background-color: $gray-1250; border-radius: 2px; diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx index ec9968674..7955d43ec 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx @@ -22,6 +22,7 @@ interface MilestoneRowProps { rowIndex: number; table: Table; injectedStyles?: CSSProperties; + hasCursor?: boolean; } export default function MilestoneRow({ @@ -32,6 +33,7 @@ export default function MilestoneRow({ colour, rowId, rowIndex, + hasCursor, table, injectedStyles, ...virtuosoProps @@ -64,6 +66,7 @@ export default function MilestoneRow({ '--user-bg': parentBgColour ?? 'transparent', }} data-testid='cuesheet-milestone' + data-cursor={hasCursor} {...virtuosoProps} > {cuesheetMode === AppMode.Edit && ( diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx index 8745d9a5e..0577cb982 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx @@ -3,7 +3,7 @@ import { IoAdd, IoArrowDown, IoArrowUp, IoDuplicateOutline, IoOptions, IoTrash } import { SupportedEntry } from 'ontime-types'; import { PositionedDropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu'; -import { useEntryActions } from '../../../../common/hooks/useEntryAction'; +import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext'; import { useCuesheetEditModal } from '../../cuesheet-edit-modal/useCuesheetEditModal'; import { useCuesheetPermissions } from '../../useTablePermissions'; @@ -13,7 +13,7 @@ export default memo(CuesheetTableMenu); function CuesheetTableMenu() { const { isOpen, entryId, entryIndex, parentId, flag, position, closeMenu } = useCuesheetTableMenu(); - const { addEntry, clone, deleteEntry, move, updateEntry } = useEntryActions(); + const { addEntry, clone, deleteEntry, move, updateEntry } = useEntryActionsContext(); const showModal = useCuesheetEditModal((state) => state.setEditableEntry); const permissions = useCuesheetPermissions(); diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/EditorTableMenu.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/EditorTableMenu.tsx index 6acf01d47..ad5ccfe38 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/EditorTableMenu.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/EditorTableMenu.tsx @@ -3,7 +3,7 @@ import { IoAdd, IoArrowDown, IoArrowUp, IoDuplicateOutline, IoOptions, IoTrash } import { SupportedEntry } from 'ontime-types'; import { PositionedDropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu'; -import { useEntryActions } from '../../../../common/hooks/useEntryAction'; +import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext'; import { useCuesheetEditModal } from '../../cuesheet-edit-modal/useCuesheetEditModal'; import { useCuesheetTableMenu } from './useCuesheetTableMenu'; @@ -12,7 +12,7 @@ export default memo(EditorTableMenu); function EditorTableMenu() { const { isOpen, entryId, entryIndex, parentId, flag, position, closeMenu } = useCuesheetTableMenu(); - const { addEntry, clone, deleteEntry, move, updateEntry } = useEntryActions(); + const { addEntry, clone, deleteEntry, move, updateEntry } = useEntryActionsContext(); const showModal = useCuesheetEditModal((state) => state.setEditableEntry); if (!isOpen) { diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.tsx index 8436438d6..a7ea67fdb 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.tsx @@ -1,5 +1,5 @@ import { ReactNode, use } from 'react'; -import { IoChevronDown, IoOptions, IoSettingsOutline } from 'react-icons/io5'; +import { IoBookOutline, IoChevronDown, IoOptions } from 'react-icons/io5'; import { Popover } from '@base-ui/react/popover'; import { Toggle } from '@base-ui/react/toggle'; import { ToggleGroup } from '@base-ui/react/toggle-group'; @@ -100,7 +100,7 @@ export function ViewSettings({ optionsStore }: ViewSettingsProps) { - Settings + Settings } @@ -162,7 +162,7 @@ export function ColumnSettings({ - View + Columns } @@ -183,7 +183,6 @@ export function ColumnSettings({ ); })}
-
Reset Options