-
- {isExtracted &&
}
-
-
+
+
+
+ {isExtracted &&
}
+
+
+
+ {!isExtracted && handleLinks('rundown', event)} />}
+
+
+
+
+ {!hideSideBar && (
+
- {!isExtracted && handleLinks('rundown', event)} />}
-
-
+
-
- {!hideSideBar && (
-
-
-
-
-
- )}
-
+
+ )}
-
-
+
+
);
}
@@ -105,8 +97,6 @@ interface RundownRootProps {
}
function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: RundownRootProps) {
- const source = useLoadedRundownSource();
-
return (
{isSmallDevice ? (
@@ -115,7 +105,7 @@ function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: Rund
)}
{viewMode === RundownViewMode.List ? : }
- {viewMode === RundownViewMode.Table && }
+ {viewMode === RundownViewMode.Table && }
);
diff --git a/apps/client/src/features/rundown/RundownList.tsx b/apps/client/src/features/rundown/RundownList.tsx
index 86d80906b..f6c3113a5 100644
--- a/apps/client/src/features/rundown/RundownList.tsx
+++ b/apps/client/src/features/rundown/RundownList.tsx
@@ -1,15 +1,22 @@
+import { Playback } from 'ontime-types';
import { memo } from 'react';
import EmptyFill from '../../common/components/state/EmptyFill';
+import { useRundownScope } from '../../common/context/RundownScopeContext';
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
import { useRundownEditor } from '../../common/hooks/useSocket';
import { useTranslation } from '../../translation/TranslationProvider';
import Rundown from './Rundown';
+/** a rundown which is not loaded has no playback to follow */
+const idleFeatureData = { playback: Playback.Stop, selectedEventId: null, nextEventId: null };
+
export default memo(RundownList);
function RundownList() {
+ const { isLoaded } = useRundownScope();
const { data, status, rundownMetadata } = useRundownWithMetadata();
- const featureData = useRundownEditor();
+ const runtimeFeatureData = useRundownEditor();
+ const featureData = isLoaded ? runtimeFeatureData : idleFeatureData;
const { getLocalizedString } = useTranslation();
// avoid showing the editable empty state before we know whether the rundown is actually empty
diff --git a/apps/client/src/features/rundown/hooks/useRundownCommands.ts b/apps/client/src/features/rundown/hooks/useRundownCommands.ts
index e036aa1ce..e6fd1375c 100644
--- a/apps/client/src/features/rundown/hooks/useRundownCommands.ts
+++ b/apps/client/src/features/rundown/hooks/useRundownCommands.ts
@@ -2,6 +2,7 @@ import { type EntryId, type OntimeEntry, type Rundown, SupportedEntry } from 'on
import { getNextGroupNormal, getNextNormal, getPreviousGroupNormal, getPreviousNormal } from 'ontime-utils';
import { useCallback } from 'react';
+import { useRundownScope } from '../../../common/context/RundownScopeContext';
import type { useEntryActions } from '../../../common/hooks/useEntryAction';
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
import { SelectionMode } from '../useEventSelection';
@@ -26,6 +27,7 @@ export function useRundownCommands({
selectEntry: applySelection,
handleCollapseGroup,
}: UseRundownCommandsOptions) {
+ const { rundownId } = useRundownScope();
const { addEntry, clone, deleteEntry, move, reorderEntry } = entryActions;
const deleteAtCursor = useCallback(
@@ -43,12 +45,17 @@ export function useRundownCommands({
const insertCopyAtId = useCallback(
(atId: EntryId | null, above = false) => {
// lazily get the value from the store
- const { entryCopyId, entryCopyMode, setEntryCopyId } = useEntryCopy.getState();
+ const { entryCopyId, entryCopyRundownId, entryCopyMode, setEntryCopyId } = useEntryCopy.getState();
if (entryCopyId === null || !entries[entryCopyId]) {
// we cant clone without selection
return;
}
+ // pasting into a different rundown needs the entry payload, the server clones within a rundown
+ if (entryCopyRundownId !== null && entryCopyRundownId !== rundownId) {
+ return;
+ }
+
let normalisedAtId = atId;
const elementToCopy = entries[entryCopyId];
@@ -65,7 +72,7 @@ export function useRundownCommands({
return;
}
reorderEntry(entryCopyId, firstId, 'before')
- .then(() => setEntryCopyId(null))
+ .then(() => setEntryCopyId(null, null))
.catch(() => {});
return;
}
@@ -74,7 +81,7 @@ export function useRundownCommands({
}
const placement = above ? 'before' : 'after';
reorderEntry(entryCopyId, normalisedAtId, placement)
- .then(() => setEntryCopyId(null))
+ .then(() => setEntryCopyId(null, null))
.catch(() => {});
return;
}
@@ -85,7 +92,7 @@ export function useRundownCommands({
before: above ? (normalisedAtId ?? undefined) : undefined,
});
},
- [entries, flatOrder, clone, reorderEntry],
+ [entries, flatOrder, clone, reorderEntry, rundownId],
);
/**
diff --git a/apps/client/src/features/rundown/rundown-table/RundownTable.tsx b/apps/client/src/features/rundown/rundown-table/RundownTable.tsx
index 03f28a277..1c3dd7df7 100644
--- a/apps/client/src/features/rundown/rundown-table/RundownTable.tsx
+++ b/apps/client/src/features/rundown/rundown-table/RundownTable.tsx
@@ -1,9 +1,6 @@
import { memo, useEffect, useMemo } from 'react';
-import { EntryActionsProvider } from '../../../common/context/EntryActionsContext';
import useCustomFields from '../../../common/hooks-query/useCustomFields';
-import { useLoadedRundownSource } from '../../../common/hooks-query/useScopedRundown';
-import { useEntryActions } from '../../../common/hooks/useEntryAction';
import CuesheetDnd from '../../../views/cuesheet/cuesheet-dnd/CuesheetDnd';
import CuesheetTable from '../../../views/cuesheet/cuesheet-table/CuesheetTable';
import { useCuesheetPermissions } from '../../../views/cuesheet/useTablePermissions';
@@ -15,8 +12,6 @@ function RundownTable() {
const { data: customFields } = useCustomFields();
const setPermissions = useCuesheetPermissions((state) => state.setPermissions);
const { editorMode } = useEditorFollowMode();
- const source = useLoadedRundownSource();
- const actions = useEntryActions();
// Editor always has full permissions
useEffect(() => {
@@ -32,10 +27,8 @@ function RundownTable() {
const columns = useMemo(() => makeRundownColumns(customFields), [customFields]);
return (
-
-
-
-
-
+
+
+
);
}
diff --git a/apps/client/src/features/rundown/useEventSelection.ts b/apps/client/src/features/rundown/useEventSelection.ts
index 180f525f2..c5c60eabe 100644
--- a/apps/client/src/features/rundown/useEventSelection.ts
+++ b/apps/client/src/features/rundown/useEventSelection.ts
@@ -1,155 +1,15 @@
-import { EntryId, MaybeNumber, ProjectRundownsList, Rundown, isOntimeEvent } from 'ontime-types';
-import { MouseEvent } from 'react';
-import { create } from 'zustand';
+import { useStore } from 'zustand';
-import { PROJECT_RUNDOWNS, getRundownQueryKey } from '../../common/api/constants';
-import { ontimeQueryClient } from '../../common/queryClient';
-import { isMacOS } from '../../common/utils/deviceUtils';
+import { useRundownScope } from '../../common/context/RundownScopeContext';
+import type { EventSelectionStore } from '../../common/stores/eventSelectionStore';
-export type SelectionMode = 'shift' | 'click' | 'ctrl';
-
-interface EventSelectionStore {
- selectedEvents: Set
;
- anchoredIndex: MaybeNumber;
- cursor: EntryId | null;
- entryMode: 'event' | 'single' | null;
- scrollHandler: ((id: EntryId) => void) | null;
- setSingleEntrySelection: (selectionArgs: { id: EntryId }) => void;
- setSelectedEvents: (selectionArgs: { id: EntryId; index: number; selectMode: SelectionMode }) => void;
- clearSelectedEvents: () => void;
- clearMultiSelect: () => void;
- unselect: (id: EntryId) => void;
- setScrollHandler: (handler: ((id: EntryId) => void) | null) => void;
- scrollToEntry: (id: EntryId) => void;
-}
+export { getSelectionMode } from '../../common/stores/eventSelectionStore';
+export type { SelectionMode } from '../../common/stores/eventSelectionStore';
/**
- * Keeps track of the selected entries and selection mode
- * Provides methods to update the selection based on user interactions
+ * Selection and cursor for the rundown of the enclosing scope
*/
-export const useEventSelection = create()((set, get) => ({
- selectedEvents: new Set(),
- anchoredIndex: null,
- cursor: null,
- entryMode: null,
- scrollHandler: null,
- setSingleEntrySelection: ({ id }) => {
- set({ selectedEvents: new Set([id]), anchoredIndex: null, cursor: id, entryMode: 'single' });
- },
- setSelectedEvents: ({ id, index, selectMode }) => {
- const { selectedEvents, anchoredIndex, entryMode } = get();
-
- // if we are in single mode, we replace the selection and change the mode
- if (entryMode === 'single') {
- return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id, entryMode: 'event' });
- }
-
- // on click, we replace selection with event
- if (selectMode === 'click') {
- return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id, entryMode: 'event' });
- }
-
- // on ctrl + click, we toggle the selection of that event
- if (selectMode === 'ctrl') {
- const rundownData = getLoadedRundownData();
- if (!rundownData) return;
-
- // if it doesnt exist, simply add to the list and set an anchor
- if (!selectedEvents.has(id)) {
- return set({
- selectedEvents: selectedEvents.add(id),
- anchoredIndex: index,
- cursor: id,
- entryMode: 'event',
- });
- }
-
- // if event is already selected, we remove it from selection
- // and set the anchor to the event after
- selectedEvents.delete(id);
-
- const nextIndex = rundownData.order.findIndex(
- (eventId, i) => i > index && isOntimeEvent(rundownData.entries[eventId]) && selectedEvents.has(eventId),
- );
-
- // if we didnt find anything after, set the anchor to the last event
- return set({
- selectedEvents,
- anchoredIndex: nextIndex < 0 ? rundownData.order.length - 1 : nextIndex,
- entryMode: 'event',
- });
- }
-
- // on shift + click, we select a range of events up to the clicked event
- if (selectMode === 'shift') {
- const rundownData = getLoadedRundownData();
- if (!rundownData) return;
-
- // get list of rundown with only ontime events
- const eventIds: EntryId[] = [];
- rundownData.flatOrder.forEach((eventId) => {
- const event = rundownData.entries[eventId];
- if (isOntimeEvent(event)) {
- eventIds.push(event.id);
- }
- });
-
- const start = anchoredIndex === null ? 0 : Math.min(anchoredIndex, index);
- const end = anchoredIndex === null ? index : Math.max(anchoredIndex, index + 1);
-
- // create new set with range of ids from start to end
- const selectedEventIds = eventIds.slice(start, end);
-
- return set({
- selectedEvents: new Set([...selectedEvents, ...selectedEventIds]),
- anchoredIndex: index,
- entryMode: 'event',
- });
- }
- },
- clearSelectedEvents: () => set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null }),
- clearMultiSelect: () => {
- const { selectedEvents } = get();
- const [firstSelected] = selectedEvents;
- set({
- selectedEvents: new Set(firstSelected ? [firstSelected] : []),
- anchoredIndex: null,
- entryMode: null,
- });
- },
- unselect: (id: string) => {
- const { entryMode, selectedEvents } = get();
- selectedEvents.delete(id);
- set({
- selectedEvents,
- entryMode: selectedEvents.size === 0 ? null : entryMode,
- });
- },
- // Sets the scroll handler for programmatic scrolling to entries
- setScrollHandler: (handler) => set({ scrollHandler: handler }),
- // Scrolls to the specified entry using the registered scroll handler
- scrollToEntry: (id: EntryId) => {
- const handler = get().scrollHandler;
- if (handler) {
- handler(id);
- }
- },
-}));
-
-function getLoadedRundownData() {
- const rundownId = ontimeQueryClient.getQueryData(PROJECT_RUNDOWNS)?.loaded;
- if (!rundownId) return undefined;
- return ontimeQueryClient.getQueryData(getRundownQueryKey(rundownId));
-}
-
-export function getSelectionMode(event: MouseEvent): SelectionMode {
- if ((isMacOS() && event.metaKey) || event.ctrlKey) {
- return 'ctrl';
- }
-
- if (event.shiftKey) {
- return 'shift';
- }
-
- return 'click';
+export function useEventSelection(selector: (state: EventSelectionStore) => T): T {
+ const { selectionStore } = useRundownScope();
+ return useStore(selectionStore, selector);
}
diff --git a/apps/client/src/views/cuesheet/CuesheetPage.tsx b/apps/client/src/views/cuesheet/CuesheetPage.tsx
index d2febeb17..7be760bc5 100644
--- a/apps/client/src/views/cuesheet/CuesheetPage.tsx
+++ b/apps/client/src/views/cuesheet/CuesheetPage.tsx
@@ -3,35 +3,31 @@ import { IoApps } from 'react-icons/io5';
import IconButton from '../../common/components/buttons/IconButton';
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
-import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
-import { useScopedRundown } from '../../common/hooks-query/useScopedRundown';
-import { useScopedEntryActions } from '../../common/hooks/useEntryAction';
+import { EditableRundownScopeProvider } from '../../common/context/EditableRundownScopeProvider';
+import { useRundownSelection } from '../../common/hooks/useRundownSelection';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import { getIsNavigationLocked } from '../../externals';
import CuesheetOverview from '../../features/overview/CuesheetOverview';
import EntryEditModal from './cuesheet-edit-modal/EntryEditModal';
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
import CuesheetTableWrapper from './CuesheetTableWrapper';
-import { FOLLOW_LOADED_RUNDOWN_ID, useCuesheetRundownSelection } from './useCuesheetRundownSelection';
import styles from './CuesheetPage.module.scss';
export default function CuesheetPage() {
'use memo';
const [isMenuOpen, menuHandler] = useDisclosure();
- const { selectedRundownId, loadedRundownId, setSelectedRundownId, projectRundowns } = useCuesheetRundownSelection();
- const source = useScopedRundown(selectedRundownId === FOLLOW_LOADED_RUNDOWN_ID ? loadedRundownId : selectedRundownId);
-
- const actions = useScopedEntryActions(source.rundownId);
+ const { scopedRundownId, selectedRundownId, loadedRundownId, setSelectedRundownId, projectRundowns } =
+ useRundownSelection('cuesheet');
useWindowTitle('Cuesheet');
const isLocked = getIsNavigationLocked();
return (
-
+
-
+
{!isLocked && (
@@ -42,13 +38,12 @@ export default function CuesheetPage() {
-
+
);
}
diff --git a/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx b/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx
index 048f8f1c6..86f7bcea2 100644
--- a/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx
+++ b/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx
@@ -3,19 +3,18 @@ import { memo, use, useMemo } from 'react';
import Select from '../../common/components/select/Select';
import { PresetContext } from '../../common/context/PresetContext';
+import { useRundownScope } from '../../common/context/RundownScopeContext';
import useCustomFields from '../../common/hooks-query/useCustomFields';
-import type { RundownSource } from '../../common/hooks-query/useScopedRundown';
+import { FOLLOW_LOADED_RUNDOWN_ID } from '../../common/hooks/useRundownSelection';
import { AppMode } from '../../ontimeConfig';
import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetColsFactory';
import CuesheetTable from './cuesheet-table/CuesheetTable';
import { useApplyCuesheetPolicy } from './useApplyCuesheetPolicy';
-import { FOLLOW_LOADED_RUNDOWN_ID } from './useCuesheetRundownSelection';
import styles from './CuesheetPage.module.scss';
interface CuesheetTableWrapperProps {
- source: RundownSource;
selectedRundownId: MaybeString;
loadedRundownId: string;
setSelectedRundownId: (rundownId: string) => void;
@@ -24,15 +23,14 @@ interface CuesheetTableWrapperProps {
export default memo(CuesheetTableWrapper);
function CuesheetTableWrapper({
- source,
selectedRundownId,
setSelectedRundownId,
loadedRundownId,
projectRundowns,
}: CuesheetTableWrapperProps) {
const preset = use(PresetContext);
- const isCurrentRundown = source.rundownId !== null && source.rundownId === loadedRundownId;
- const { cuesheetMode, setCuesheetMode } = useApplyCuesheetPolicy(preset, { canRunMode: isCurrentRundown });
+ const { isLoaded } = useRundownScope();
+ const { cuesheetMode, setCuesheetMode } = useApplyCuesheetPolicy(preset, { canRunMode: isLoaded });
const { data: customFields } = useCustomFields();
const columns = useMemo(
@@ -44,11 +42,9 @@ function CuesheetTableWrapper({
state.selectedEntryId);
const closeModal = useEditModal((state) => state.clearSelection);
diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx
index 76031ae67..6a562ae16 100644
--- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx
+++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx
@@ -14,7 +14,8 @@ import {
import EmptyFill from '../../../common/components/state/EmptyFill';
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
-import type { RundownSource } from '../../../common/hooks-query/useScopedRundown';
+import { useRundownScope } from '../../../common/context/RundownScopeContext';
+import { useFlatRundownWithMetadata, useScopedSelectedEventId } from '../../../common/hooks-query/useRundown';
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options';
import { useEventSelection } from '../../../features/rundown/useEventSelection';
@@ -41,20 +42,17 @@ import style from './CuesheetTable.module.scss';
type CuesheetTableBaseProps = {
columns: CuesheetColumnDef[];
cuesheetMode: AppMode;
- source: RundownSource;
insertElement?: ReactNode;
};
type EditorCuesheetTableProps = CuesheetTableBaseProps & {
tableRoot: 'editor';
setCuesheetMode?: undefined;
- isCurrentRundown?: undefined;
};
type ViewCuesheetTableProps = CuesheetTableBaseProps & {
tableRoot: 'cuesheet';
setCuesheetMode: (mode: AppMode) => void;
- isCurrentRundown?: boolean;
};
type CuesheetTableProps = EditorCuesheetTableProps | ViewCuesheetTableProps;
@@ -62,13 +60,13 @@ type CuesheetTableProps = EditorCuesheetTableProps | ViewCuesheetTableProps;
export default function CuesheetTable({
columns,
cuesheetMode,
- source,
tableRoot,
setCuesheetMode,
- isCurrentRundown,
insertElement,
}: CuesheetTableProps) {
- const { flatRundown, status, selectedEventId } = source;
+ const { isLoaded: isCurrentRundown } = useRundownScope();
+ const { data: flatRundown, status } = useFlatRundownWithMetadata();
+ const selectedEventId = useScopedSelectedEventId();
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
const { getLocalizedString } = useTranslation();
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
diff --git a/apps/client/src/views/editor/Editor.tsx b/apps/client/src/views/editor/Editor.tsx
index 66d848c65..94ae7d3d9 100644
--- a/apps/client/src/views/editor/Editor.tsx
+++ b/apps/client/src/views/editor/Editor.tsx
@@ -1,5 +1,6 @@
import { lazy } from 'react';
+import { EditableRundownScopeProvider } from '../../common/context/EditableRundownScopeProvider';
import TrackingPlaybackBar from '../../features/control/playback/tracking-playback-bar/TrackingPlaybackBar';
import { AppMode } from '../../ontimeConfig';
import TitleList from './title-list/TitleList';
@@ -21,7 +22,9 @@ export default function Editor() {
-