mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 14:14:17 +00:00
feat: select rundown in list view
This commit is contained in:
@@ -1,14 +1,7 @@
|
|||||||
import { Maybe, ProjectRundown } from 'ontime-types';
|
import { Maybe, ProjectRundown } from 'ontime-types';
|
||||||
import {
|
import { PropsWithChildren, createContext, startTransition, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||||
PropsWithChildren,
|
import { useSearchParams } from 'react-router';
|
||||||
createContext,
|
import { useNavigate } from 'react-router';
|
||||||
startTransition,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { useProjectRundowns } from '../hooks-query/useProjectRundowns';
|
import { useProjectRundowns } from '../hooks-query/useProjectRundowns';
|
||||||
|
|
||||||
@@ -27,7 +20,8 @@ export function RundownSelectionContextProvider({ children }: PropsWithChildren)
|
|||||||
'use memo';
|
'use memo';
|
||||||
const { data } = useProjectRundowns();
|
const { data } = useProjectRundowns();
|
||||||
const { loaded, rundowns } = data;
|
const { loaded, rundowns } = data;
|
||||||
const [selectedRundownId, setSelectedRundownId] = useState<Maybe<string>>(null);
|
|
||||||
|
const [selectedRundownId, setSelectedRundownId] = useSelectRundownFromParams();
|
||||||
|
|
||||||
const selectRundownId = useCallback(
|
const selectRundownId = useCallback(
|
||||||
(rundownId: Maybe<string>) => {
|
(rundownId: Maybe<string>) => {
|
||||||
@@ -36,7 +30,7 @@ export function RundownSelectionContextProvider({ children }: PropsWithChildren)
|
|||||||
else setSelectedRundownId(null);
|
else setSelectedRundownId(null);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[rundowns],
|
[rundowns, setSelectedRundownId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const effectiveRundownId = selectedRundownId ? selectedRundownId : loaded;
|
const effectiveRundownId = selectedRundownId ? selectedRundownId : loaded;
|
||||||
@@ -44,7 +38,7 @@ export function RundownSelectionContextProvider({ children }: PropsWithChildren)
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rundowns.find((entry) => entry.id === effectiveRundownId)) setSelectedRundownId(null);
|
if (!rundowns.find((entry) => entry.id === effectiveRundownId)) setSelectedRundownId(null);
|
||||||
}, [rundowns, effectiveRundownId]);
|
}, [rundowns, effectiveRundownId, setSelectedRundownId]);
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
(): RundownScopeValue => ({
|
(): RundownScopeValue => ({
|
||||||
@@ -70,3 +64,57 @@ export function useRundownSelectionContext() {
|
|||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rundownParam = 'rundownId';
|
||||||
|
|
||||||
|
export function useSelectRundownFromParams(): [Maybe<string>, (id: Maybe<string>) => void] {
|
||||||
|
'use memo';
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const selectedRundownId = searchParams.get(rundownParam);
|
||||||
|
const setSelectedRundownId = useCallback(
|
||||||
|
(id: Maybe<string>) => {
|
||||||
|
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<string>, 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],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ import { DropdownMenu } from '../../../../common/components/dropdown-menu/Dropdo
|
|||||||
import Tag from '../../../../common/components/tag/Tag';
|
import Tag from '../../../../common/components/tag/Tag';
|
||||||
import { useMutateProjectRundowns, useProjectRundowns } from '../../../../common/hooks-query/useProjectRundowns';
|
import { useMutateProjectRundowns, useProjectRundowns } from '../../../../common/hooks-query/useProjectRundowns';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import { useDirectLinkToBackgroundEdit } from '../../../../views/cuesheet/useCuesheetRundownSelection';
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import RundownRenameForm from './composite/RundownRenameForm';
|
import RundownRenameForm from './composite/RundownRenameForm';
|
||||||
import { ManageRundownForm } from './ManageRundownForm';
|
import { ManageRundownForm } from './ManageRundownForm';
|
||||||
|
|
||||||
import style from './ManagePanel.module.scss';
|
import style from './ManagePanel.module.scss';
|
||||||
|
import { useDirectLinkToBackgroundEdit } from '../../../../common/context/RundownSelectionContext';
|
||||||
|
|
||||||
export default function ManageRundowns() {
|
export default function ManageRundowns() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: Rund
|
|||||||
{isSmallDevice ? (
|
{isSmallDevice ? (
|
||||||
<RundownHeaderMobile viewMode={viewMode} setViewMode={setViewMode} />
|
<RundownHeaderMobile viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
) : (
|
) : (
|
||||||
|
// TODO: add data-background-rundown={!isLoadedRundown} styling
|
||||||
<RundownHeader isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
|
<RundownHeader isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
)}
|
)}
|
||||||
{viewMode === RundownViewMode.List ? <RundownList /> : <RundownTable />}
|
{viewMode === RundownViewMode.List ? <RundownList /> : <RundownTable />}
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
import Select from '../../../../common/components/select/Select';
|
import Select from '../../../common/components/select/Select';
|
||||||
import { useRundownSelectionContext } from '../../../../common/context/RundownSelectionContext';
|
import { useRundownSelectionContext } from '../../../common/context/RundownSelectionContext';
|
||||||
import { AppMode } from '../../../../ontimeConfig';
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
|
|
||||||
import styles from './RundownSelect.module.scss';
|
import styles from './RundownSelect.module.scss';
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import Tooltip from '../../../common/components/tooltip/Tooltip';
|
|||||||
import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket';
|
import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket';
|
||||||
import { AppMode } from '../../../ontimeConfig';
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
import { EditorLayoutMode, useEditorLayout } from '../../../views/editor/useEditorLayout';
|
import { EditorLayoutMode, useEditorLayout } from '../../../views/editor/useEditorLayout';
|
||||||
|
import { RundownSelect } from '../common/RundownSelect';
|
||||||
import { RundownViewMode } from '../rundown.options';
|
import { RundownViewMode } from '../rundown.options';
|
||||||
import { useEditorFollowMode } from '../useEditorFollowMode';
|
import { useEditorFollowMode } from '../useEditorFollowMode';
|
||||||
import RundownMenu from './RundownMenu';
|
import RundownMenu from './RundownMenu';
|
||||||
@@ -24,6 +25,7 @@ interface HeaderControlsConfig {
|
|||||||
showRunEditToggle: boolean;
|
showRunEditToggle: boolean;
|
||||||
showOffsetToggle: boolean;
|
showOffsetToggle: boolean;
|
||||||
showOverflowMenu: boolean;
|
showOverflowMenu: boolean;
|
||||||
|
showRundownSelect: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HEADER_CONTROLS_CONFIG: Record<EditorLayoutMode, HeaderControlsConfig> = {
|
export const HEADER_CONTROLS_CONFIG: Record<EditorLayoutMode, HeaderControlsConfig> = {
|
||||||
@@ -31,16 +33,19 @@ export const HEADER_CONTROLS_CONFIG: Record<EditorLayoutMode, HeaderControlsConf
|
|||||||
showRunEditToggle: true,
|
showRunEditToggle: true,
|
||||||
showOffsetToggle: true,
|
showOffsetToggle: true,
|
||||||
showOverflowMenu: true,
|
showOverflowMenu: true,
|
||||||
|
showRundownSelect: false,
|
||||||
},
|
},
|
||||||
[EditorLayoutMode.PLANNING]: {
|
[EditorLayoutMode.PLANNING]: {
|
||||||
showRunEditToggle: false,
|
showRunEditToggle: false,
|
||||||
showOffsetToggle: false,
|
showOffsetToggle: false,
|
||||||
showOverflowMenu: true,
|
showOverflowMenu: true,
|
||||||
|
showRundownSelect: true,
|
||||||
},
|
},
|
||||||
[EditorLayoutMode.TRACKING]: {
|
[EditorLayoutMode.TRACKING]: {
|
||||||
showRunEditToggle: false,
|
showRunEditToggle: false,
|
||||||
showOffsetToggle: true,
|
showOffsetToggle: true,
|
||||||
showOverflowMenu: false,
|
showOverflowMenu: false,
|
||||||
|
showRundownSelect: false,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -50,7 +55,8 @@ function RundownHeader({ isExtracted, viewMode, setViewMode }: RundownHeaderProp
|
|||||||
const offsetMode = useOffsetMode();
|
const offsetMode = useOffsetMode();
|
||||||
const { layoutMode } = useEditorLayout();
|
const { layoutMode } = useEditorLayout();
|
||||||
|
|
||||||
const { showRunEditToggle, showOffsetToggle, showOverflowMenu } = HEADER_CONTROLS_CONFIG[layoutMode];
|
const { showRunEditToggle, showOffsetToggle, showOverflowMenu, showRundownSelect } =
|
||||||
|
HEADER_CONTROLS_CONFIG[layoutMode];
|
||||||
|
|
||||||
const toggleAppMode = (mode: AppMode[]) => {
|
const toggleAppMode = (mode: AppMode[]) => {
|
||||||
// we need to stop user from deselecting a mode
|
// we need to stop user from deselecting a mode
|
||||||
@@ -122,6 +128,8 @@ function RundownHeader({ isExtracted, viewMode, setViewMode }: RundownHeaderProp
|
|||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showRundownSelect && <RundownSelect appMode={editorMode} />}
|
||||||
|
|
||||||
{showOverflowMenu && <RundownMenu allowNavigation={!isExtracted} />}
|
{showOverflowMenu && <RundownMenu allowNavigation={!isExtracted} />}
|
||||||
</Toolbar.Root>
|
</Toolbar.Root>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function CuesheetPage() {
|
|||||||
const isLocked = getIsNavigationLocked();
|
const isLocked = getIsNavigationLocked();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RundownSelectionContextProvider>
|
<RundownSelectionContextProvider >
|
||||||
<EntryActionsProvider>
|
<EntryActionsProvider>
|
||||||
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
|
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
|
||||||
<EntryEditModal />
|
<EntryEditModal />
|
||||||
|
|||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+3
-2
@@ -12,10 +12,10 @@ import * as Editor from '../../../../common/components/editor-utils/EditorUtils'
|
|||||||
import PopoverContents from '../../../../common/components/popover/Popover';
|
import PopoverContents from '../../../../common/components/popover/Popover';
|
||||||
import { useRundownSelectionContext } from '../../../../common/context/RundownSelectionContext';
|
import { useRundownSelectionContext } from '../../../../common/context/RundownSelectionContext';
|
||||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||||
|
import { RundownSelect } from '../../../../features/rundown/common/RundownSelect';
|
||||||
import { AppMode } from '../../../../ontimeConfig';
|
import { AppMode } from '../../../../ontimeConfig';
|
||||||
import { useCuesheetPermissions } from '../../useTablePermissions';
|
import { useCuesheetPermissions } from '../../useTablePermissions';
|
||||||
import CuesheetShareModal from './CuesheetShareModal';
|
import CuesheetShareModal from './CuesheetShareModal';
|
||||||
import { RundownSelect } from './RundownSelect';
|
|
||||||
|
|
||||||
import style from './CuesheetTableSettings.module.scss';
|
import style from './CuesheetTableSettings.module.scss';
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ export default function CuesheetTableHeaderToolbar({
|
|||||||
}: CuesheetTableHeaderToolbarProps) {
|
}: CuesheetTableHeaderToolbarProps) {
|
||||||
const canChangeMode = useCuesheetPermissions((state) => state.canChangeMode) && tableRoot === 'cuesheet';
|
const canChangeMode = useCuesheetPermissions((state) => state.canChangeMode) && tableRoot === 'cuesheet';
|
||||||
const canShare = useCuesheetPermissions((state) => state.canShare) && tableRoot === 'cuesheet';
|
const canShare = useCuesheetPermissions((state) => state.canShare) && tableRoot === 'cuesheet';
|
||||||
|
const showRundownSelect = tableRoot === 'cuesheet';
|
||||||
const { isLoadedRundown } = useRundownSelectionContext();
|
const { isLoadedRundown } = useRundownSelectionContext();
|
||||||
|
|
||||||
const toggleCuesheetMode = (mode: AppMode[]) => {
|
const toggleCuesheetMode = (mode: AppMode[]) => {
|
||||||
@@ -73,7 +74,7 @@ export default function CuesheetTableHeaderToolbar({
|
|||||||
handleClearToggles={handleClearToggles}
|
handleClearToggles={handleClearToggles}
|
||||||
/>
|
/>
|
||||||
<div className={style.apart}>
|
<div className={style.apart}>
|
||||||
<RundownSelect appMode={appMode} />
|
{showRundownSelect && <RundownSelect appMode={appMode} />}
|
||||||
{canChangeMode && (
|
{canChangeMode && (
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
value={[appMode]}
|
value={[appMode]}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import { useSessionStorage } from '@mantine/hooks';
|
|
||||||
import { startTransition, useCallback, useMemo } from 'react';
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
|
|
||||||
import { useOrderedProjectList } from '../../common/hooks-query/useProjectList';
|
|
||||||
import { useProjectRundowns } from '../../common/hooks-query/useProjectRundowns';
|
|
||||||
import { serverURL } from '../../externals';
|
|
||||||
|
|
||||||
export const FOLLOW_LOADED_RUNDOWN_ID = '__follow-loaded__' as const;
|
|
||||||
|
|
||||||
export function getCuesheetRundownStorageKey(server: string, projectFilename: string) {
|
|
||||||
return `cuesheet-selected-rundown:${server}:${projectFilename}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveSelectedRundownId(storedSelectedRundownId: string | null, availableRundownIds: Set<string>) {
|
|
||||||
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<string | null>({
|
|
||||||
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<string | null>({ key: storageKey, defaultValue: null });
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async (rundownId: string) => {
|
|
||||||
await navigate('/cuesheet');
|
|
||||||
startTransition(() => setStoredSelectedRundownId(rundownId));
|
|
||||||
},
|
|
||||||
[setStoredSelectedRundownId, navigate],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { isValueOfEnum } from 'ontime-utils';
|
import { isValueOfEnum } from 'ontime-utils';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
|
import { setSelectRundownInParams } from '../../common/context/RundownSelectionContext';
|
||||||
|
|
||||||
const layoutParam = 'layout';
|
const layoutParam = 'layout';
|
||||||
|
|
||||||
export enum EditorLayoutMode {
|
export enum EditorLayoutMode {
|
||||||
@@ -20,14 +23,29 @@ function getEditorLayout(value: string | null): EditorLayoutMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useEditorLayout() {
|
export function useEditorLayout() {
|
||||||
|
'use memo';
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const layoutMode = getEditorLayout(searchParams.get(layoutParam));
|
const layoutMode = getEditorLayout(searchParams.get(layoutParam));
|
||||||
|
|
||||||
const setLayoutMode = (mode: EditorLayoutMode) => {
|
useEffect(() => {
|
||||||
const nextParams = new URLSearchParams(searchParams);
|
setSearchParams((searchParams) => {
|
||||||
nextParams.set(layoutParam, mode);
|
if (layoutMode !== EditorLayoutMode.PLANNING) setSelectRundownInParams(null, searchParams);
|
||||||
setSearchParams(nextParams, { replace: true });
|
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 };
|
return { layoutMode, setLayoutMode };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user