feat: url navigation for settings (#821)

This commit is contained in:
Carlos Valente
2024-03-18 20:36:02 +01:00
committed by GitHub
parent 4b21745fc7
commit dfade25a7c
11 changed files with 147 additions and 61 deletions
@@ -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>
</> </>
); );
} }
@@ -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 };
}
+10 -8
View File
@@ -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>