diff --git a/apps/client/src/common/context/RundownSelectionContext.tsx b/apps/client/src/common/context/RundownSelectionContext.tsx index 403160327..f205128ec 100644 --- a/apps/client/src/common/context/RundownSelectionContext.tsx +++ b/apps/client/src/common/context/RundownSelectionContext.tsx @@ -1,14 +1,7 @@ import { Maybe, ProjectRundown } from 'ontime-types'; -import { - PropsWithChildren, - createContext, - startTransition, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react'; +import { PropsWithChildren, createContext, startTransition, useCallback, useContext, useEffect, useMemo } from 'react'; +import { useSearchParams } from 'react-router'; +import { useNavigate } from 'react-router'; import { useProjectRundowns } from '../hooks-query/useProjectRundowns'; @@ -27,7 +20,8 @@ export function RundownSelectionContextProvider({ children }: PropsWithChildren) 'use memo'; const { data } = useProjectRundowns(); const { loaded, rundowns } = data; - const [selectedRundownId, setSelectedRundownId] = useState>(null); + + const [selectedRundownId, setSelectedRundownId] = useSelectRundownFromParams(); const selectRundownId = useCallback( (rundownId: Maybe) => { @@ -36,7 +30,7 @@ export function RundownSelectionContextProvider({ children }: PropsWithChildren) else setSelectedRundownId(null); }); }, - [rundowns], + [rundowns, setSelectedRundownId], ); const effectiveRundownId = selectedRundownId ? selectedRundownId : loaded; @@ -44,7 +38,7 @@ export function RundownSelectionContextProvider({ children }: PropsWithChildren) useEffect(() => { if (!rundowns.find((entry) => entry.id === effectiveRundownId)) setSelectedRundownId(null); - }, [rundowns, effectiveRundownId]); + }, [rundowns, effectiveRundownId, setSelectedRundownId]); const value = useMemo( (): RundownScopeValue => ({ @@ -70,3 +64,57 @@ export function useRundownSelectionContext() { return context; } + +const rundownParam = 'rundownId'; + +export function useSelectRundownFromParams(): [Maybe, (id: Maybe) => void] { + 'use memo'; + const [searchParams, setSearchParams] = useSearchParams(); + + const selectedRundownId = searchParams.get(rundownParam); + const setSelectedRundownId = useCallback( + (id: Maybe) => { + if (id === null) { + setSearchParams((searchParams) => { + searchParams.delete(rundownParam); + return searchParams; + }); + } else { + setSearchParams((searchParams) => { + searchParams.set(rundownParam, id); + return searchParams; + }); + } + }, + [setSearchParams], + ); + + return [selectedRundownId, setSelectedRundownId]; +} + +/** + * + * mutates the provided `searchParams` + */ +export function setSelectRundownInParams(id: Maybe, searchParams: URLSearchParams): void { + if (id === null) { + searchParams.delete(rundownParam); + } else { + searchParams.set(rundownParam, id); + } +} + +export function useDirectLinkToBackgroundEdit() { + const navigate = useNavigate(); + const [search] = useSearchParams(); + return useCallback( + async (rundownId: string) => { + setSelectRundownInParams(rundownId, search); + navigate({ + pathname: '/cuesheet', + search: search.toString(), + }); + }, + [navigate, search], + ); +} diff --git a/apps/client/src/features/app-settings/panel/manage-panel/ManageRundowns.tsx b/apps/client/src/features/app-settings/panel/manage-panel/ManageRundowns.tsx index 48bfcbbbd..247bd0a82 100644 --- a/apps/client/src/features/app-settings/panel/manage-panel/ManageRundowns.tsx +++ b/apps/client/src/features/app-settings/panel/manage-panel/ManageRundowns.tsx @@ -20,12 +20,12 @@ import { DropdownMenu } from '../../../../common/components/dropdown-menu/Dropdo import Tag from '../../../../common/components/tag/Tag'; import { useMutateProjectRundowns, useProjectRundowns } from '../../../../common/hooks-query/useProjectRundowns'; import { cx } from '../../../../common/utils/styleUtils'; -import { useDirectLinkToBackgroundEdit } from '../../../../views/cuesheet/useCuesheetRundownSelection'; import * as Panel from '../../panel-utils/PanelUtils'; import RundownRenameForm from './composite/RundownRenameForm'; import { ManageRundownForm } from './ManageRundownForm'; import style from './ManagePanel.module.scss'; +import { useDirectLinkToBackgroundEdit } from '../../../../common/context/RundownSelectionContext'; export default function ManageRundowns() { return ( diff --git a/apps/client/src/features/rundown/RundownExport.tsx b/apps/client/src/features/rundown/RundownExport.tsx index 6ba4f2caa..51c4e39f0 100644 --- a/apps/client/src/features/rundown/RundownExport.tsx +++ b/apps/client/src/features/rundown/RundownExport.tsx @@ -112,6 +112,7 @@ function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: Rund {isSmallDevice ? ( ) : ( + // TODO: add data-background-rundown={!isLoadedRundown} styling )} {viewMode === RundownViewMode.List ? : } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/RundownSelect.module.scss b/apps/client/src/features/rundown/common/RundownSelect.module.scss similarity index 100% rename from apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/RundownSelect.module.scss rename to apps/client/src/features/rundown/common/RundownSelect.module.scss diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/RundownSelect.tsx b/apps/client/src/features/rundown/common/RundownSelect.tsx similarity index 81% rename from apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/RundownSelect.tsx rename to apps/client/src/features/rundown/common/RundownSelect.tsx index 21d13d3c9..ba4349068 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/RundownSelect.tsx +++ b/apps/client/src/features/rundown/common/RundownSelect.tsx @@ -1,6 +1,6 @@ -import Select from '../../../../common/components/select/Select'; -import { useRundownSelectionContext } from '../../../../common/context/RundownSelectionContext'; -import { AppMode } from '../../../../ontimeConfig'; +import Select from '../../../common/components/select/Select'; +import { useRundownSelectionContext } from '../../../common/context/RundownSelectionContext'; +import { AppMode } from '../../../ontimeConfig'; import styles from './RundownSelect.module.scss'; diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx index d7316211c..e6668080f 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx +++ b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx @@ -8,6 +8,7 @@ import Tooltip from '../../../common/components/tooltip/Tooltip'; import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket'; import { AppMode } from '../../../ontimeConfig'; import { EditorLayoutMode, useEditorLayout } from '../../../views/editor/useEditorLayout'; +import { RundownSelect } from '../common/RundownSelect'; import { RundownViewMode } from '../rundown.options'; import { useEditorFollowMode } from '../useEditorFollowMode'; import RundownMenu from './RundownMenu'; @@ -24,6 +25,7 @@ interface HeaderControlsConfig { showRunEditToggle: boolean; showOffsetToggle: boolean; showOverflowMenu: boolean; + showRundownSelect: boolean; } export const HEADER_CONTROLS_CONFIG: Record = { @@ -31,16 +33,19 @@ export const HEADER_CONTROLS_CONFIG: Record { // we need to stop user from deselecting a mode @@ -122,6 +128,8 @@ function RundownHeader({ isExtracted, viewMode, setViewMode }: RundownHeaderProp )} + {showRundownSelect && } + {showOverflowMenu && } ); diff --git a/apps/client/src/views/cuesheet/CuesheetPage.tsx b/apps/client/src/views/cuesheet/CuesheetPage.tsx index cc31f5d2a..4169ed477 100644 --- a/apps/client/src/views/cuesheet/CuesheetPage.tsx +++ b/apps/client/src/views/cuesheet/CuesheetPage.tsx @@ -23,7 +23,7 @@ export default function CuesheetPage() { const isLocked = getIsNavigationLocked(); return ( - + diff --git a/apps/client/src/views/cuesheet/__tests__/useCuesheetRundownSelection.test.ts b/apps/client/src/views/cuesheet/__tests__/useCuesheetRundownSelection.test.ts deleted file mode 100644 index 4baaeb2b4..000000000 --- a/apps/client/src/views/cuesheet/__tests__/useCuesheetRundownSelection.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - FOLLOW_LOADED_RUNDOWN_ID, - getCuesheetRundownStorageKey, - resolveSelectedRundownId, -} from '../useCuesheetRundownSelection'; - -describe('useCuesheetRundownSelection helpers', () => { - it('builds a project-scoped storage key', () => { - expect(getCuesheetRundownStorageKey('http://localhost:4001', 'My Project')).toBe( - 'cuesheet-selected-rundown:http://localhost:4001:My Project', - ); - }); - - it('falls back to the follow loaded rundown when the stored selection is missing', () => { - expect(resolveSelectedRundownId('missing', new Set(['loaded', 'other']))).toBe(FOLLOW_LOADED_RUNDOWN_ID); - }); - - it('keeps the stored selection when it still exists in the current project', () => { - expect(resolveSelectedRundownId('other', new Set(['loaded', 'other']))).toBe('other'); - }); -}); diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableHeaderToolbar.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableHeaderToolbar.tsx index 8b5993b27..e07bde1a4 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableHeaderToolbar.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableHeaderToolbar.tsx @@ -12,10 +12,10 @@ import * as Editor from '../../../../common/components/editor-utils/EditorUtils' import PopoverContents from '../../../../common/components/popover/Popover'; import { useRundownSelectionContext } from '../../../../common/context/RundownSelectionContext'; import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata'; +import { RundownSelect } from '../../../../features/rundown/common/RundownSelect'; import { AppMode } from '../../../../ontimeConfig'; import { useCuesheetPermissions } from '../../useTablePermissions'; import CuesheetShareModal from './CuesheetShareModal'; -import { RundownSelect } from './RundownSelect'; import style from './CuesheetTableSettings.module.scss'; @@ -55,6 +55,7 @@ export default function CuesheetTableHeaderToolbar({ }: CuesheetTableHeaderToolbarProps) { const canChangeMode = useCuesheetPermissions((state) => state.canChangeMode) && tableRoot === 'cuesheet'; const canShare = useCuesheetPermissions((state) => state.canShare) && tableRoot === 'cuesheet'; + const showRundownSelect = tableRoot === 'cuesheet'; const { isLoadedRundown } = useRundownSelectionContext(); const toggleCuesheetMode = (mode: AppMode[]) => { @@ -73,7 +74,7 @@ export default function CuesheetTableHeaderToolbar({ handleClearToggles={handleClearToggles} />
- + {showRundownSelect && } {canChangeMode && ( ) { - if (storedSelectedRundownId && availableRundownIds.has(storedSelectedRundownId)) return storedSelectedRundownId; - return FOLLOW_LOADED_RUNDOWN_ID; -} - -export function useCuesheetRundownSelection() { - 'use memo'; - - const { data: projectRundowns } = useProjectRundowns(); - const { - data: { lastLoadedProject }, - } = useOrderedProjectList(); - const storageKey = useMemo(() => getCuesheetRundownStorageKey(serverURL, lastLoadedProject), [lastLoadedProject]); - const [storedSelectedRundownId, setStoredSelectedRundownId] = useSessionStorage({ - key: storageKey, - defaultValue: FOLLOW_LOADED_RUNDOWN_ID, - }); - - const availableRundownIds = new Set(projectRundowns.rundowns.map(({ id }) => id)).add(FOLLOW_LOADED_RUNDOWN_ID); - const { loaded: loadedRundownId } = projectRundowns; - - const selectedRundownId = resolveSelectedRundownId(storedSelectedRundownId, availableRundownIds); - - return { - loadedRundownId, - selectedRundownId, - projectRundowns: projectRundowns.rundowns, - setSelectedRundownId: (rundownId: string) => { - startTransition(() => { - setStoredSelectedRundownId(rundownId); - }); - }, - }; -} - -export function useDirectLinkToBackgroundEdit() { - const { - data: { lastLoadedProject }, - } = useOrderedProjectList(); - const navigate = useNavigate(); - const storageKey = getCuesheetRundownStorageKey(serverURL, lastLoadedProject); - const [_, setStoredSelectedRundownId] = useSessionStorage({ key: storageKey, defaultValue: null }); - - return useCallback( - async (rundownId: string) => { - await navigate('/cuesheet'); - startTransition(() => setStoredSelectedRundownId(rundownId)); - }, - [setStoredSelectedRundownId, navigate], - ); -} diff --git a/apps/client/src/views/editor/useEditorLayout.ts b/apps/client/src/views/editor/useEditorLayout.ts index 454ae7326..4c6a9c5b5 100644 --- a/apps/client/src/views/editor/useEditorLayout.ts +++ b/apps/client/src/views/editor/useEditorLayout.ts @@ -1,6 +1,9 @@ import { isValueOfEnum } from 'ontime-utils'; +import { useCallback, useEffect } from 'react'; import { useSearchParams } from 'react-router'; +import { setSelectRundownInParams } from '../../common/context/RundownSelectionContext'; + const layoutParam = 'layout'; export enum EditorLayoutMode { @@ -20,14 +23,29 @@ function getEditorLayout(value: string | null): EditorLayoutMode { } export function useEditorLayout() { + 'use memo'; + const [searchParams, setSearchParams] = useSearchParams(); const layoutMode = getEditorLayout(searchParams.get(layoutParam)); - const setLayoutMode = (mode: EditorLayoutMode) => { - const nextParams = new URLSearchParams(searchParams); - nextParams.set(layoutParam, mode); - setSearchParams(nextParams, { replace: true }); - }; + useEffect(() => { + setSearchParams((searchParams) => { + if (layoutMode !== EditorLayoutMode.PLANNING) setSelectRundownInParams(null, searchParams); + return searchParams; + }); + }, [setSearchParams, layoutMode]); + + const setLayoutMode = useCallback( + (mode: EditorLayoutMode) => { + setSearchParams((searchParams) => { + searchParams.set(layoutParam, mode); + // Only the Planning layout is allowed to look at something other than the current rundown + if (mode !== EditorLayoutMode.PLANNING) setSelectRundownInParams(null, searchParams); + return searchParams; + }); + }, + [setSearchParams], + ); return { layoutMode, setLayoutMode }; }