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 PanelContent from './panel-content/PanelContent';
import PanelList from './panel-list/PanelList';
import { useSettingsStore } from './settingsStore';
import useAppSettingsNavigation from './useAppSettingsNavigation';
import style from './AppSettings.module.scss';
export default function AppSettings() {
const setShowSettings = useSettingsStore((state) => state.setShowSettings);
const selectedPanel = useSettingsStore((state) => state.showSettings);
const closeSettings = () => {
setShowSettings(null);
};
useKeyDown(closeSettings, 'Escape');
const { close, panel, location } = useAppSettingsNavigation();
useKeyDown(close, 'Escape');
return (
<div className={style.container}>
<ErrorBoundary>
<PanelList />
<PanelContent onClose={closeSettings}>
{selectedPanel === 'project' && <ProjectPanel />}
{selectedPanel === 'general' && <GeneralPanel />}
{selectedPanel === 'project_settings' && <ProjectSettingsPanel />}
{selectedPanel === 'sources' && <SourcesPanel />}
{selectedPanel === 'interface' && <InterfacePanel />}
{selectedPanel === 'integrations' && <IntegrationsPanel />}
{selectedPanel === 'about' && <AboutPanel />}
{selectedPanel === 'log' && <LogPanel />}
<PanelList selectedPanel={panel} location={location} />
<PanelContent onClose={close}>
{panel === 'project' && <ProjectPanel location={location} />}
{panel === 'general' && <GeneralPanel location={location} />}
{panel === 'project_settings' && <ProjectSettingsPanel />}
{panel === 'sources' && <SourcesPanel />}
{panel === 'interface' && <InterfacePanel />}
{panel === 'integrations' && <IntegrationsPanel location={location} />}
{panel === 'about' && <AboutPanel />}
{panel === 'log' && <LogPanel />}
</PanelContent>
</ErrorBoundary>
</div>
@@ -58,4 +58,8 @@ ul {
color: $secondary-text-gray;
border-left: 1px solid $white-10;
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 { 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';
export default function PanelList() {
const { showSettings, setShowSettings, hasUnsavedChanges } = useSettingsStore();
interface PanelListProps extends PanelBaseProps {
selectedPanel: string;
}
const handleSelect = (panel: SettingsOption) => {
setShowSettings(panel.id);
};
export default function PanelList({ selectedPanel, location }: PanelListProps) {
const { setLocation } = useAppSettingsNavigation();
const { hasUnsavedChanges } = useSettingsStore();
return (
<ul className={style.tabs}>
@@ -20,7 +22,7 @@ export default function PanelList() {
const classes = cx([
style.primary,
showSettings === panel.id ? style.active : null,
selectedPanel === panel.id ? style.active : null,
panel.split ? style.split : null,
unsaved ? style.unsaved : null,
]);
@@ -29,9 +31,9 @@ export default function PanelList() {
<Fragment key={panel.id}>
<li
key={panel.id}
onClick={() => handleSelect(panel)}
onClick={() => setLocation(panel.id)}
onKeyDown={(event) => {
isKeyEnter(event) && handleSelect(panel);
isKeyEnter(event) && setLocation(panel.id);
}}
className={classes}
tabIndex={0}
@@ -40,8 +42,18 @@ export default function PanelList() {
{panel.label}
</li>
{panel.secondary?.map((secondary) => {
const id = secondary.id.split('__')[1];
const secondaryClasses = cx([style.secondary, location === id ? style.active : null]);
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}
</li>
);
@@ -1,16 +1,28 @@
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { PanelBaseProps } from '../../settingsStore';
import * as Panel from '../PanelUtils';
import GeneralPanelForm from './GeneralPanelForm';
import UrlPresetsForm from './UrlPresetsForm';
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 (
<>
<Panel.Header>Settings</Panel.Header>
<GeneralPanelForm />
<ViewSettingsForm />
<UrlPresetsForm />
<div ref={manageRef}>
<GeneralPanelForm />
</div>
<div ref={viewRef}>
<ViewSettingsForm />
</div>
<div ref={urlPresetsRef}>
<UrlPresetsForm />
</div>
</>
);
}
@@ -1,6 +1,8 @@
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { PanelBaseProps } from '../../settingsStore';
import * as Panel from '../PanelUtils';
import HttpIntegrations from './HttpIntegrations';
@@ -8,7 +10,10 @@ import OscIntegrations from './OscIntegrations';
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 (
<>
<Panel.Header>Integration settings</Panel.Header>
@@ -25,8 +30,12 @@ export default function IntegrationsPanel() {
</Alert>
</Panel.Section>
<Panel.Section>
<OscIntegrations />
<HttpIntegrations />
<div ref={oscRef}>
<OscIntegrations />
</div>
<div ref={httpRef}>
<HttpIntegrations />
</div>
</Panel.Section>
</>
);
@@ -1,14 +1,23 @@
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { PanelBaseProps } from '../../settingsStore';
import * as Panel from '../PanelUtils';
import ManageProjects from './ManageProjects';
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 (
<>
<Panel.Header>Project</Panel.Header>
<ProjectData />
<ManageProjects />
<div ref={projectRef}>
<ProjectData />
</div>
<div ref={manageRef}>
<ManageProjects />
</div>
</>
);
}
@@ -19,7 +19,7 @@ export const settingPanels: Readonly<SettingsOption[]> = [
secondary: [
{ id: 'general__manage', label: 'Manage Ontime 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;
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
const firstPanel = settingPanels[0].id;
export interface PanelBaseProps {
location?: string;
}
type SettingsStore = {
showSettings: SettingsOptionId | null;
setShowSettings: (panelId?: SettingsOptionId | null) => void;
unsavedChanges: Set<SettingsOptionId>;
hasUnsavedChanges: (panelId: SettingsOptionId) => boolean;
addUnsavedChanges: (panelId: SettingsOptionId) => void;
@@ -66,16 +66,6 @@ type SettingsStore = {
};
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(),
hasUnsavedChanges: (panelId: SettingsOptionId) => get().unsavedChanges.has(panelId),
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 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 Overview from '../overview/Overview';
@@ -13,11 +14,14 @@ const TimerControl = lazy(() => import('../control/playback/TimerControlExport')
const MessageControl = lazy(() => import('../control/message/MessageControlExport'));
export default function Editor() {
const showSettings = useSettingsStore((state) => state.showSettings);
const setShowSettings = useSettingsStore((state) => state.setShowSettings);
const { isOpen, setLocation, close } = useAppSettingsNavigation();
const handleSettings = (newTab?: SettingsOptionId) => {
setShowSettings(newTab);
if (isOpen) {
close();
} else {
setLocation(newTab ?? 'project');
}
};
// Set window title
@@ -25,14 +29,12 @@ export default function Editor() {
document.title = 'ontime - Editor';
}, []);
const isSettingsOpen = Boolean(showSettings);
return (
<div className={styles.mainContainer} data-testid='event-editor'>
<ErrorBoundary>
<MenuBar openSettings={handleSettings} isSettingsOpen={isSettingsOpen} />
<MenuBar openSettings={handleSettings} isSettingsOpen={isOpen} />
</ErrorBoundary>
{showSettings ? (
{isOpen ? (
<AppSettings />
) : (
<div id='panels' className={styles.panelContainer}>
@@ -1,4 +1,5 @@
import { CSSProperties, useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Button } from '@chakra-ui/react';
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
@@ -25,6 +26,7 @@ export default function EventEditor() {
const { data: customFields } = useCustomFields();
const { order, rundown } = data;
const { updateEvent } = useEventAction();
const [_searchParams, setSearchParams] = useSearchParams();
const [event, setEvent] = useState<OntimeEvent | null>(null);
@@ -60,6 +62,10 @@ export default function EventEditor() {
[event?.id, updateEvent],
);
const handleOpenCustomManager = () => {
setSearchParams({ settings: 'project_settings__custom' });
};
if (!event) {
return (
<div className={style.eventEditor} data-testid='editor-container'>
@@ -98,7 +104,7 @@ export default function EventEditor() {
<div className={style.column}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span>Custom Fields</span>
<Button variant='ontime-subtle' size='sm' isDisabled>
<Button variant='ontime-subtle' size='sm' onClick={handleOpenCustomManager}>
Manage
</Button>
</div>