mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-03 05:19:08 +00:00
feat: url navigation for settings (#821)
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export default function useScrollIntoView<T extends HTMLElement>(name: string, location?: string) {
|
||||||
|
const ref = useRef<T>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location && ref.current) {
|
||||||
|
if (location === name) {
|
||||||
|
ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [location, name]);
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
@@ -12,32 +12,27 @@ import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettings
|
|||||||
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
||||||
import PanelContent from './panel-content/PanelContent';
|
import PanelContent from './panel-content/PanelContent';
|
||||||
import PanelList from './panel-list/PanelList';
|
import PanelList from './panel-list/PanelList';
|
||||||
import { useSettingsStore } from './settingsStore';
|
import useAppSettingsNavigation from './useAppSettingsNavigation';
|
||||||
|
|
||||||
import style from './AppSettings.module.scss';
|
import style from './AppSettings.module.scss';
|
||||||
|
|
||||||
export default function AppSettings() {
|
export default function AppSettings() {
|
||||||
const setShowSettings = useSettingsStore((state) => state.setShowSettings);
|
const { close, panel, location } = useAppSettingsNavigation();
|
||||||
const selectedPanel = useSettingsStore((state) => state.showSettings);
|
useKeyDown(close, 'Escape');
|
||||||
|
|
||||||
const closeSettings = () => {
|
|
||||||
setShowSettings(null);
|
|
||||||
};
|
|
||||||
useKeyDown(closeSettings, 'Escape');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PanelList />
|
<PanelList selectedPanel={panel} location={location} />
|
||||||
<PanelContent onClose={closeSettings}>
|
<PanelContent onClose={close}>
|
||||||
{selectedPanel === 'project' && <ProjectPanel />}
|
{panel === 'project' && <ProjectPanel location={location} />}
|
||||||
{selectedPanel === 'general' && <GeneralPanel />}
|
{panel === 'general' && <GeneralPanel location={location} />}
|
||||||
{selectedPanel === 'project_settings' && <ProjectSettingsPanel />}
|
{panel === 'project_settings' && <ProjectSettingsPanel />}
|
||||||
{selectedPanel === 'sources' && <SourcesPanel />}
|
{panel === 'sources' && <SourcesPanel />}
|
||||||
{selectedPanel === 'interface' && <InterfacePanel />}
|
{panel === 'interface' && <InterfacePanel />}
|
||||||
{selectedPanel === 'integrations' && <IntegrationsPanel />}
|
{panel === 'integrations' && <IntegrationsPanel location={location} />}
|
||||||
{selectedPanel === 'about' && <AboutPanel />}
|
{panel === 'about' && <AboutPanel />}
|
||||||
{selectedPanel === 'log' && <LogPanel />}
|
{panel === 'log' && <LogPanel />}
|
||||||
</PanelContent>
|
</PanelContent>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,4 +58,8 @@ ul {
|
|||||||
color: $secondary-text-gray;
|
color: $secondary-text-gray;
|
||||||
border-left: 1px solid $white-10;
|
border-left: 1px solid $white-10;
|
||||||
font-size: $inner-section-text-size;
|
font-size: $inner-section-text-size;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $blue-400;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ import { Fragment } from 'react';
|
|||||||
|
|
||||||
import { isKeyEnter } from '../../../common/utils/keyEvent';
|
import { isKeyEnter } from '../../../common/utils/keyEvent';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
import { settingPanels, SettingsOption, useSettingsStore } from '../settingsStore';
|
import { PanelBaseProps, settingPanels, useSettingsStore } from '../settingsStore';
|
||||||
|
import useAppSettingsNavigation from '../useAppSettingsNavigation';
|
||||||
|
|
||||||
import style from './PanelList.module.scss';
|
import style from './PanelList.module.scss';
|
||||||
|
|
||||||
export default function PanelList() {
|
interface PanelListProps extends PanelBaseProps {
|
||||||
const { showSettings, setShowSettings, hasUnsavedChanges } = useSettingsStore();
|
selectedPanel: string;
|
||||||
|
}
|
||||||
|
|
||||||
const handleSelect = (panel: SettingsOption) => {
|
export default function PanelList({ selectedPanel, location }: PanelListProps) {
|
||||||
setShowSettings(panel.id);
|
const { setLocation } = useAppSettingsNavigation();
|
||||||
};
|
const { hasUnsavedChanges } = useSettingsStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={style.tabs}>
|
<ul className={style.tabs}>
|
||||||
@@ -20,7 +22,7 @@ export default function PanelList() {
|
|||||||
|
|
||||||
const classes = cx([
|
const classes = cx([
|
||||||
style.primary,
|
style.primary,
|
||||||
showSettings === panel.id ? style.active : null,
|
selectedPanel === panel.id ? style.active : null,
|
||||||
panel.split ? style.split : null,
|
panel.split ? style.split : null,
|
||||||
unsaved ? style.unsaved : null,
|
unsaved ? style.unsaved : null,
|
||||||
]);
|
]);
|
||||||
@@ -29,9 +31,9 @@ export default function PanelList() {
|
|||||||
<Fragment key={panel.id}>
|
<Fragment key={panel.id}>
|
||||||
<li
|
<li
|
||||||
key={panel.id}
|
key={panel.id}
|
||||||
onClick={() => handleSelect(panel)}
|
onClick={() => setLocation(panel.id)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
isKeyEnter(event) && handleSelect(panel);
|
isKeyEnter(event) && setLocation(panel.id);
|
||||||
}}
|
}}
|
||||||
className={classes}
|
className={classes}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -40,8 +42,18 @@ export default function PanelList() {
|
|||||||
{panel.label}
|
{panel.label}
|
||||||
</li>
|
</li>
|
||||||
{panel.secondary?.map((secondary) => {
|
{panel.secondary?.map((secondary) => {
|
||||||
|
const id = secondary.id.split('__')[1];
|
||||||
|
const secondaryClasses = cx([style.secondary, location === id ? style.active : null]);
|
||||||
return (
|
return (
|
||||||
<li key={secondary.id} onClick={() => handleSelect(panel)} className={style.secondary} role='button'>
|
<li
|
||||||
|
key={secondary.id}
|
||||||
|
onClick={() => setLocation(secondary.id)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && setLocation(secondary.id);
|
||||||
|
}}
|
||||||
|
className={secondaryClasses}
|
||||||
|
role='button'
|
||||||
|
>
|
||||||
{secondary.label}
|
{secondary.label}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
|
import { PanelBaseProps } from '../../settingsStore';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import GeneralPanelForm from './GeneralPanelForm';
|
import GeneralPanelForm from './GeneralPanelForm';
|
||||||
import UrlPresetsForm from './UrlPresetsForm';
|
import UrlPresetsForm from './UrlPresetsForm';
|
||||||
import ViewSettingsForm from './ViewSettingsForm';
|
import ViewSettingsForm from './ViewSettingsForm';
|
||||||
|
|
||||||
export default function GeneralPanel() {
|
export default function GeneralPanel({ location }: PanelBaseProps) {
|
||||||
|
const manageRef = useScrollIntoView<HTMLDivElement>('manage', location);
|
||||||
|
const viewRef = useScrollIntoView<HTMLDivElement>('view', location);
|
||||||
|
const urlPresetsRef = useScrollIntoView<HTMLDivElement>('urlpresets', location);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>Settings</Panel.Header>
|
<Panel.Header>Settings</Panel.Header>
|
||||||
<GeneralPanelForm />
|
<div ref={manageRef}>
|
||||||
<ViewSettingsForm />
|
<GeneralPanelForm />
|
||||||
<UrlPresetsForm />
|
</div>
|
||||||
|
<div ref={viewRef}>
|
||||||
|
<ViewSettingsForm />
|
||||||
|
</div>
|
||||||
|
<div ref={urlPresetsRef}>
|
||||||
|
<UrlPresetsForm />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -1,6 +1,8 @@
|
|||||||
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
||||||
|
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
|
import { PanelBaseProps } from '../../settingsStore';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import HttpIntegrations from './HttpIntegrations';
|
import HttpIntegrations from './HttpIntegrations';
|
||||||
@@ -8,7 +10,10 @@ import OscIntegrations from './OscIntegrations';
|
|||||||
|
|
||||||
const integrationDocsUrl = 'https://docs.getontime.no/api/integrations/';
|
const integrationDocsUrl = 'https://docs.getontime.no/api/integrations/';
|
||||||
|
|
||||||
export default function IntegrationsPanel() {
|
export default function IntegrationsPanel({ location }: PanelBaseProps) {
|
||||||
|
const oscRef = useScrollIntoView<HTMLDivElement>('osc', location);
|
||||||
|
const httpRef = useScrollIntoView<HTMLDivElement>('http', location);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>Integration settings</Panel.Header>
|
<Panel.Header>Integration settings</Panel.Header>
|
||||||
@@ -25,8 +30,12 @@ export default function IntegrationsPanel() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<OscIntegrations />
|
<div ref={oscRef}>
|
||||||
<HttpIntegrations />
|
<OscIntegrations />
|
||||||
|
</div>
|
||||||
|
<div ref={httpRef}>
|
||||||
|
<HttpIntegrations />
|
||||||
|
</div>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
|
import { PanelBaseProps } from '../../settingsStore';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import ManageProjects from './ManageProjects';
|
import ManageProjects from './ManageProjects';
|
||||||
import ProjectData from './ProjectData';
|
import ProjectData from './ProjectData';
|
||||||
|
|
||||||
export default function ProjectPanel() {
|
export default function ProjectPanel({ location }: PanelBaseProps) {
|
||||||
|
const projectRef = useScrollIntoView<HTMLDivElement>('project', location);
|
||||||
|
const manageRef = useScrollIntoView<HTMLDivElement>('manage', location);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>Project</Panel.Header>
|
<Panel.Header>Project</Panel.Header>
|
||||||
<ProjectData />
|
<div ref={projectRef}>
|
||||||
<ManageProjects />
|
<ProjectData />
|
||||||
|
</div>
|
||||||
|
<div ref={manageRef}>
|
||||||
|
<ManageProjects />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
|||||||
secondary: [
|
secondary: [
|
||||||
{ id: 'general__manage', label: 'Manage Ontime settings' },
|
{ id: 'general__manage', label: 'Manage Ontime settings' },
|
||||||
{ id: 'general__view', label: 'View settings' },
|
{ id: 'general__view', label: 'View settings' },
|
||||||
{ id: 'general__urlPresets', label: 'URL presets' },
|
{ id: 'general__urlpresets', label: 'URL presets' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -54,11 +54,11 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
|
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
|
||||||
const firstPanel = settingPanels[0].id;
|
export interface PanelBaseProps {
|
||||||
|
location?: string;
|
||||||
|
}
|
||||||
|
|
||||||
type SettingsStore = {
|
type SettingsStore = {
|
||||||
showSettings: SettingsOptionId | null;
|
|
||||||
setShowSettings: (panelId?: SettingsOptionId | null) => void;
|
|
||||||
unsavedChanges: Set<SettingsOptionId>;
|
unsavedChanges: Set<SettingsOptionId>;
|
||||||
hasUnsavedChanges: (panelId: SettingsOptionId) => boolean;
|
hasUnsavedChanges: (panelId: SettingsOptionId) => boolean;
|
||||||
addUnsavedChanges: (panelId: SettingsOptionId) => void;
|
addUnsavedChanges: (panelId: SettingsOptionId) => void;
|
||||||
@@ -66,16 +66,6 @@ type SettingsStore = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||||
showSettings: null,
|
|
||||||
setShowSettings: (panelId?: SettingsOptionId | null) => {
|
|
||||||
const newPanel = panelId === undefined ? firstPanel : panelId;
|
|
||||||
set((state) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
showSettings: newPanel,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
unsavedChanges: new Set(),
|
unsavedChanges: new Set(),
|
||||||
hasUnsavedChanges: (panelId: SettingsOptionId) => get().unsavedChanges.has(panelId),
|
hasUnsavedChanges: (panelId: SettingsOptionId) => get().unsavedChanges.has(panelId),
|
||||||
addUnsavedChanges: (panelId: SettingsOptionId) =>
|
addUnsavedChanges: (panelId: SettingsOptionId) =>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { SettingsOptionId } from './settingsStore';
|
||||||
|
|
||||||
|
const settingsKey = 'settings';
|
||||||
|
|
||||||
|
export default function useAppSettingsNavigation() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const selectedPanel = useMemo(
|
||||||
|
() => (searchParams.get(settingsKey) as SettingsOptionId | null) ?? 'project',
|
||||||
|
[searchParams],
|
||||||
|
);
|
||||||
|
const isOpen = useMemo(() => Boolean(searchParams.get(settingsKey)), [searchParams]);
|
||||||
|
const [panel, location] = selectedPanel.split('__');
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
searchParams.delete(settingsKey);
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
|
const setLocation = useCallback(
|
||||||
|
(panelId: SettingsOptionId) => {
|
||||||
|
searchParams.set(settingsKey, panelId);
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
},
|
||||||
|
[searchParams, setSearchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isOpen, panel, location, setLocation, close };
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@ import { lazy, useEffect } from 'react';
|
|||||||
|
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
import AppSettings from '../app-settings/AppSettings';
|
import AppSettings from '../app-settings/AppSettings';
|
||||||
import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStore';
|
import { SettingsOptionId } from '../app-settings/settingsStore';
|
||||||
|
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
||||||
import MenuBar from '../menu/MenuBar';
|
import MenuBar from '../menu/MenuBar';
|
||||||
import Overview from '../overview/Overview';
|
import Overview from '../overview/Overview';
|
||||||
|
|
||||||
@@ -13,11 +14,14 @@ const TimerControl = lazy(() => import('../control/playback/TimerControlExport')
|
|||||||
const MessageControl = lazy(() => import('../control/message/MessageControlExport'));
|
const MessageControl = lazy(() => import('../control/message/MessageControlExport'));
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const showSettings = useSettingsStore((state) => state.showSettings);
|
const { isOpen, setLocation, close } = useAppSettingsNavigation();
|
||||||
const setShowSettings = useSettingsStore((state) => state.setShowSettings);
|
|
||||||
|
|
||||||
const handleSettings = (newTab?: SettingsOptionId) => {
|
const handleSettings = (newTab?: SettingsOptionId) => {
|
||||||
setShowSettings(newTab);
|
if (isOpen) {
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
setLocation(newTab ?? 'project');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
@@ -25,14 +29,12 @@ export default function Editor() {
|
|||||||
document.title = 'ontime - Editor';
|
document.title = 'ontime - Editor';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isSettingsOpen = Boolean(showSettings);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<MenuBar openSettings={handleSettings} isSettingsOpen={isSettingsOpen} />
|
<MenuBar openSettings={handleSettings} isSettingsOpen={isOpen} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
{showSettings ? (
|
{isOpen ? (
|
||||||
<AppSettings />
|
<AppSettings />
|
||||||
) : (
|
) : (
|
||||||
<div id='panels' className={styles.panelContainer}>
|
<div id='panels' className={styles.panelContainer}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CSSProperties, useCallback, useEffect, useState } from 'react';
|
import { CSSProperties, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { Button } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ export default function EventEditor() {
|
|||||||
const { data: customFields } = useCustomFields();
|
const { data: customFields } = useCustomFields();
|
||||||
const { order, rundown } = data;
|
const { order, rundown } = data;
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
|
const [_searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||||
|
|
||||||
@@ -60,6 +62,10 @@ export default function EventEditor() {
|
|||||||
[event?.id, updateEvent],
|
[event?.id, updateEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleOpenCustomManager = () => {
|
||||||
|
setSearchParams({ settings: 'project_settings__custom' });
|
||||||
|
};
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return (
|
return (
|
||||||
<div className={style.eventEditor} data-testid='editor-container'>
|
<div className={style.eventEditor} data-testid='editor-container'>
|
||||||
@@ -98,7 +104,7 @@ export default function EventEditor() {
|
|||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<span>Custom Fields</span>
|
<span>Custom Fields</span>
|
||||||
<Button variant='ontime-subtle' size='sm' isDisabled>
|
<Button variant='ontime-subtle' size='sm' onClick={handleOpenCustomManager}>
|
||||||
Manage
|
Manage
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user