mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 767b567bba |
@@ -1,6 +1,10 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function useScrollIntoView<T extends HTMLElement>(name: string, location?: string) {
|
||||
export default function useScrollIntoView<T extends HTMLElement>(
|
||||
name: string,
|
||||
location?: string,
|
||||
onVisible?: (name: string) => void,
|
||||
) {
|
||||
const ref = useRef<T>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -11,5 +15,17 @@ export default function useScrollIntoView<T extends HTMLElement>(name: string, l
|
||||
}
|
||||
}, [location, name]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !onVisible) return;
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) onVisible(name);
|
||||
},
|
||||
{ threshold: 0.3, rootMargin: '0px 0px -40% 0px' },
|
||||
);
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}, [name, onVisible]);
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
gap: 0;
|
||||
|
||||
overflow: hidden;
|
||||
background-color: $ui-black;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
|
||||
import { useKeyDown } from '../../common/hooks/useKeyDown';
|
||||
import { AppSettingsScrollContext, useScrollContextState } from './AppSettingsScrollContext';
|
||||
import PanelContent from './panel-content/PanelContent';
|
||||
import PanelList from './panel-list/PanelList';
|
||||
import AboutPanel from './panel/about-panel/AboutPanel';
|
||||
@@ -17,13 +18,15 @@ import style from './AppSettings.module.scss';
|
||||
|
||||
export default function AppSettings() {
|
||||
const { close, panel, location, setLocation } = useAppSettingsNavigation();
|
||||
const scrollContext = useScrollContextState(panel);
|
||||
useKeyDown(close, 'Escape');
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<ErrorBoundary>
|
||||
<AppSettingsScrollContext.Provider value={scrollContext}>
|
||||
<PanelList selectedPanel={panel} location={location} />
|
||||
<PanelContent onClose={close}>
|
||||
<PanelContent onClose={close} panel={panel} location={location}>
|
||||
{panel === 'settings' && <SettingsPanel location={location} />}
|
||||
{panel === 'project' && <ProjectPanel location={location} setLocation={setLocation} />}
|
||||
{panel === 'manage' && <ManagePanel location={location} />}
|
||||
@@ -33,6 +36,7 @@ export default function AppSettings() {
|
||||
{panel === 'about' && <AboutPanel />}
|
||||
{panel === 'shutdown' && <ShutdownPanel />}
|
||||
</PanelContent>
|
||||
</AppSettingsScrollContext.Provider>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createContext, useCallback, useContext, useState } from 'react';
|
||||
|
||||
interface ScrollContextValue {
|
||||
activeSection: string | undefined;
|
||||
setActiveSection: (name: string) => void;
|
||||
}
|
||||
|
||||
export const AppSettingsScrollContext = createContext<ScrollContextValue>({
|
||||
activeSection: undefined,
|
||||
setActiveSection: () => {},
|
||||
});
|
||||
|
||||
export function useAppSettingsScroll() {
|
||||
return useContext(AppSettingsScrollContext);
|
||||
}
|
||||
|
||||
export function useScrollContextState(panel: string) {
|
||||
const [activeSection, setActiveSectionRaw] = useState<string | undefined>(undefined);
|
||||
const [trackedPanel, setTrackedPanel] = useState(panel);
|
||||
|
||||
if (panel !== trackedPanel) {
|
||||
setTrackedPanel(panel);
|
||||
setActiveSectionRaw(undefined);
|
||||
}
|
||||
|
||||
const setActiveSection = useCallback((name: string) => {
|
||||
setActiveSectionRaw(name);
|
||||
}, []);
|
||||
|
||||
return { activeSection, setActiveSection };
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
let _isDirty = false;
|
||||
|
||||
export const markDirty = () => {
|
||||
_isDirty = true;
|
||||
};
|
||||
|
||||
export const markClean = () => {
|
||||
_isDirty = false;
|
||||
};
|
||||
|
||||
export const getIsDirty = () => _isDirty;
|
||||
@@ -1,8 +1,19 @@
|
||||
.corner {
|
||||
position: fixed;
|
||||
top: 6rem;
|
||||
right: 4rem;
|
||||
z-index: $zindex-floating;
|
||||
.stickyHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: $zindex-nav;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: $gray-1250;
|
||||
border-bottom: 1px solid $white-10;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: calc(1rem - 1px);
|
||||
color: $gray-400;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
@@ -12,6 +23,7 @@
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -2,22 +2,31 @@ import { PropsWithChildren } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
|
||||
import Button from '../../../common/components/buttons/Button';
|
||||
import { getPanelLabel } from '../useAppSettingsMenu';
|
||||
|
||||
import style from './PanelContent.module.scss';
|
||||
|
||||
interface PanelContentProps {
|
||||
onClose: () => void;
|
||||
panel: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export default function PanelContent({ onClose, children }: PropsWithChildren<PanelContentProps>) {
|
||||
export default function PanelContent({ onClose, panel, location, children }: PropsWithChildren<PanelContentProps>) {
|
||||
const { panelLabel, sectionLabel } = getPanelLabel(panel, location);
|
||||
|
||||
return (
|
||||
<div className={style.contentWrapper}>
|
||||
<div className={style.content}>{children}</div>
|
||||
<div className={style.corner}>
|
||||
<div className={style.stickyHeader}>
|
||||
<span className={style.breadcrumb}>
|
||||
{panelLabel}
|
||||
{sectionLabel ? <> › {sectionLabel}</> : null}
|
||||
</span>
|
||||
<Button size='large' onClick={onClose}>
|
||||
Close settings <IoClose />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style.content}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
background-color: $gray-1250;
|
||||
border-right: 1px solid $white-10;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.primary,
|
||||
@@ -32,14 +35,16 @@ ul {
|
||||
|
||||
.primary {
|
||||
font-size: 1rem;
|
||||
border-radius: 2px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&.active {
|
||||
color: $blue-400;
|
||||
background-color: $gray-1100;
|
||||
border-left: 3px solid $blue-400;
|
||||
padding-left: calc(1rem - 3px);
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
&.highlight {
|
||||
@@ -63,5 +68,7 @@ ul {
|
||||
|
||||
&.active {
|
||||
color: $blue-400;
|
||||
border-left-color: $blue-400;
|
||||
border-left-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Fragment } from 'react';
|
||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||
import { isKeyEnter } from '../../../common/utils/keyEvent';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { getIsDirty, markClean } from '../appSettingsDirtyState';
|
||||
import { useAppSettingsScroll } from '../AppSettingsScrollContext';
|
||||
import { SettingsOption, SettingsOptionId, useAppSettingsMenu } from '../useAppSettingsMenu';
|
||||
import useAppSettingsNavigation from '../useAppSettingsNavigation';
|
||||
|
||||
@@ -42,18 +44,27 @@ interface PanelListItemProps {
|
||||
location?: string;
|
||||
}
|
||||
|
||||
function navigateWithGuard(navigate: (id: SettingsOptionId) => void, id: SettingsOptionId) {
|
||||
if (getIsDirty()) {
|
||||
if (!window.confirm('You have unsaved changes. Leave without saving?')) return;
|
||||
markClean();
|
||||
}
|
||||
navigate(id);
|
||||
}
|
||||
|
||||
function PanelListItem({ panel, isSelected, location }: PanelListItemProps) {
|
||||
const { setLocation } = useAppSettingsNavigation();
|
||||
const { activeSection } = useAppSettingsScroll();
|
||||
const classes = cx([style.primary, isSelected && style.active, panel.highlight && style.highlight]);
|
||||
|
||||
return (
|
||||
<Fragment key={panel.id}>
|
||||
<li
|
||||
key={panel.id}
|
||||
onClick={() => setLocation(panel.id as SettingsOptionId)}
|
||||
onClick={() => navigateWithGuard(setLocation, panel.id as SettingsOptionId)}
|
||||
onKeyDown={(event) => {
|
||||
if (isKeyEnter(event)) {
|
||||
setLocation(panel.id as SettingsOptionId);
|
||||
navigateWithGuard(setLocation, panel.id as SettingsOptionId);
|
||||
}
|
||||
}}
|
||||
className={classes}
|
||||
@@ -64,14 +75,15 @@ function PanelListItem({ panel, isSelected, location }: PanelListItemProps) {
|
||||
</li>
|
||||
{panel.secondary?.map((secondary, index) => {
|
||||
const id = secondary.id.split('__')[1];
|
||||
const secondaryClasses = cx([style.secondary, isSelected && location === id ? style.active : null]);
|
||||
const effectiveSection = isSelected ? (activeSection ?? location) : undefined;
|
||||
const secondaryClasses = cx([style.secondary, effectiveSection === id ? style.active : null]);
|
||||
return (
|
||||
<li
|
||||
key={secondary.id + index}
|
||||
onClick={() => setLocation(secondary.id as SettingsOptionId)}
|
||||
onClick={() => navigateWithGuard(setLocation, secondary.id as SettingsOptionId)}
|
||||
onKeyDown={(event) => {
|
||||
if (isKeyEnter(event)) {
|
||||
setLocation(secondary.id as SettingsOptionId);
|
||||
navigateWithGuard(setLocation, secondary.id as SettingsOptionId);
|
||||
}
|
||||
}}
|
||||
className={secondaryClasses}
|
||||
|
||||
@@ -53,9 +53,10 @@ $inner-padding: 1rem;
|
||||
.card {
|
||||
position: relative;
|
||||
padding: 2rem;
|
||||
background-color: $white-3;
|
||||
background-color: $white-5;
|
||||
border: 1px solid $gray-1100;
|
||||
border-radius: 3px;
|
||||
border-radius: 8px;
|
||||
box-shadow: $box-shadow-l1;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import { useAppSettingsScroll } from '../../AppSettingsScrollContext';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import AutomationSettingsForm from './AutomationSettingsForm';
|
||||
@@ -8,9 +9,10 @@ import TriggersList from './TriggersList';
|
||||
|
||||
export default function AutomationPanel({ location }: PanelBaseProps) {
|
||||
const { data, status } = useAutomationSettings();
|
||||
const settingsRef = useScrollIntoView<HTMLDivElement>('settings', location);
|
||||
const triggersRef = useScrollIntoView<HTMLDivElement>('triggers', location);
|
||||
const automationsRef = useScrollIntoView<HTMLDivElement>('automations', location);
|
||||
const { setActiveSection } = useAppSettingsScroll();
|
||||
const settingsRef = useScrollIntoView<HTMLDivElement>('settings', location, setActiveSection);
|
||||
const triggersRef = useScrollIntoView<HTMLDivElement>('triggers', location, setActiveSection);
|
||||
const automationsRef = useScrollIntoView<HTMLDivElement>('automations', location, setActiveSection);
|
||||
|
||||
const isLoading = status === 'pending';
|
||||
const automationState = isLoading ? undefined : data.enabledAutomations;
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import { useSettingsDirty } from '../../useSettingsDirty';
|
||||
|
||||
const oscApiDocsUrl = 'https://docs.getontime.no/api/protocols/osc/';
|
||||
|
||||
@@ -45,6 +46,7 @@ export default function AutomationSettingsForm({
|
||||
keepDirtyValues: false,
|
||||
},
|
||||
});
|
||||
useSettingsDirty(isDirty);
|
||||
|
||||
const onSubmit = async (formData: AutomationSettingsProps) => {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
import { useAppSettingsScroll } from '../../AppSettingsScrollContext';
|
||||
import GenerateLinkFormExport from '../../../sharing/GenerateLinkFormExport';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
@@ -8,9 +9,10 @@ import ReportSettings from './ReportSettings';
|
||||
import URLPresets from './URLPresets';
|
||||
|
||||
export default function FeaturePanel({ location }: PanelBaseProps) {
|
||||
const presetsRef = useScrollIntoView<HTMLDivElement>('presets', location);
|
||||
const linkRef = useScrollIntoView<HTMLDivElement>('link', location);
|
||||
const reportRef = useScrollIntoView<HTMLDivElement>('report', location);
|
||||
const { setActiveSection } = useAppSettingsScroll();
|
||||
const presetsRef = useScrollIntoView<HTMLDivElement>('presets', location, setActiveSection);
|
||||
const linkRef = useScrollIntoView<HTMLDivElement>('link', location, setActiveSection);
|
||||
const reportRef = useScrollIntoView<HTMLDivElement>('report', location, setActiveSection);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import { useAppSettingsScroll } from '../../AppSettingsScrollContext';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import CustomFieldSettings from './CustomFields';
|
||||
@@ -7,10 +8,11 @@ import RundownDefaultSettings from './RundownDefaultSettings';
|
||||
import SourcesPanel from './sources-panel/SourcesPanel';
|
||||
|
||||
export default function ManagePanel({ location }: PanelBaseProps) {
|
||||
const defaultsRef = useScrollIntoView<HTMLDivElement>('defaults', location);
|
||||
const customRef = useScrollIntoView<HTMLDivElement>('custom', location);
|
||||
const rundownsRef = useScrollIntoView<HTMLDivElement>('rundowns', location);
|
||||
const sheetsRef = useScrollIntoView<HTMLDivElement>('sheets', location);
|
||||
const { setActiveSection } = useAppSettingsScroll();
|
||||
const defaultsRef = useScrollIntoView<HTMLDivElement>('defaults', location, setActiveSection);
|
||||
const customRef = useScrollIntoView<HTMLDivElement>('custom', location, setActiveSection);
|
||||
const rundownsRef = useScrollIntoView<HTMLDivElement>('rundowns', location, setActiveSection);
|
||||
const sheetsRef = useScrollIntoView<HTMLDivElement>('sheets', location, setActiveSection);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -5,14 +5,16 @@ import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import { usePing } from '../../../../common/hooks/useSocket';
|
||||
import { sendSocket } from '../../../../common/utils/socket';
|
||||
import { isDocker } from '../../../../externals';
|
||||
import { useAppSettingsScroll } from '../../AppSettingsScrollContext';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import ClientControlPanel from './client-control/ClientControlPanel';
|
||||
import LogExport from './NetworkLogExport';
|
||||
|
||||
export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
||||
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
|
||||
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
|
||||
const { setActiveSection } = useAppSettingsScroll();
|
||||
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location, setActiveSection);
|
||||
const logRef = useScrollIntoView<HTMLDivElement>('log', location, setActiveSection);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+2
@@ -68,6 +68,7 @@ export default function ClientList() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ontimeClients.length === 0 && <Panel.TableEmpty label='No clients connected' />}
|
||||
{ontimeClients.map(([key, client]) => {
|
||||
const { identify, name, path } = client;
|
||||
const isCurrent = id === key;
|
||||
@@ -125,6 +126,7 @@ export default function ClientList() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{otherClients.length === 0 && <Panel.TableEmpty label='No other clients' />}
|
||||
{otherClients.map(([key, client]) => {
|
||||
const { name, type } = client;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import { useAppSettingsScroll } from '../../AppSettingsScrollContext';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import QuickStart from '../../quick-start/QuickStart';
|
||||
@@ -10,7 +11,8 @@ interface ProjectPanelProps extends PanelBaseProps {
|
||||
}
|
||||
|
||||
export default function ProjectPanel({ location, setLocation }: ProjectPanelProps) {
|
||||
const manageProjectsRef = useScrollIntoView<HTMLDivElement>('list', location);
|
||||
const { setActiveSection } = useAppSettingsScroll();
|
||||
const manageProjectsRef = useScrollIntoView<HTMLDivElement>('list', location, setActiveSection);
|
||||
|
||||
const handleQuickClose = () => {
|
||||
setLocation('project');
|
||||
|
||||
@@ -11,6 +11,7 @@ import Select from '../../../../common/components/select/Select';
|
||||
import useSettings from '../../../../common/hooks-query/useSettings';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import { useSettingsDirty } from '../../useSettingsDirty';
|
||||
import GeneralPinInput from './composite/GeneralPinInput';
|
||||
|
||||
const TranslationModal = lazy(() => import('./composite/CustomTranslationModal'));
|
||||
@@ -32,6 +33,7 @@ export default function GeneralSettings() {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
useSettingsDirty(isDirty);
|
||||
|
||||
const [isOpen, handler] = useDisclosure();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { validateLogo } from '../../../../common/utils/uploadUtils';
|
||||
import { documentationUrl } from '../../../../externals';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import { useSettingsDirty } from '../../useSettingsDirty';
|
||||
|
||||
import style from './SettingsPanel.module.scss';
|
||||
|
||||
@@ -38,6 +39,7 @@ export default function ProjectData() {
|
||||
},
|
||||
mode: 'onChange',
|
||||
});
|
||||
useSettingsDirty(isDirty);
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
|
||||
@@ -9,6 +9,7 @@ import useServerPort from '../../../../common/hooks-query/useServerPort';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import { useSettingsDirty } from '../../useSettingsDirty';
|
||||
|
||||
interface ServerPortForm {
|
||||
serverPort: number;
|
||||
@@ -27,6 +28,7 @@ export default function ServerPortSettings() {
|
||||
mode: 'onChange',
|
||||
defaultValues: { serverPort: 4001 },
|
||||
});
|
||||
useSettingsDirty(isDirty);
|
||||
|
||||
useEffect(() => {
|
||||
reset({ serverPort: data.port });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import { isDocker } from '../../../../externals';
|
||||
import { useAppSettingsScroll } from '../../AppSettingsScrollContext';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import CustomViews from '../manage-panel/CustomViews';
|
||||
@@ -9,11 +10,12 @@ import ServerPortSettings from './ServerPortSettings';
|
||||
import ViewSettings from './ViewSettings';
|
||||
|
||||
export default function SettingsPanel({ location }: PanelBaseProps) {
|
||||
const dataRef = useScrollIntoView<HTMLDivElement>('data', location);
|
||||
const generalRef = useScrollIntoView<HTMLDivElement>('general', location);
|
||||
const viewRef = useScrollIntoView<HTMLDivElement>('view', location);
|
||||
const customViewsRef = useScrollIntoView<HTMLDivElement>('custom-views', location);
|
||||
const portRef = useScrollIntoView<HTMLDivElement>('port', location);
|
||||
const { setActiveSection } = useAppSettingsScroll();
|
||||
const dataRef = useScrollIntoView<HTMLDivElement>('data', location, setActiveSection);
|
||||
const generalRef = useScrollIntoView<HTMLDivElement>('general', location, setActiveSection);
|
||||
const viewRef = useScrollIntoView<HTMLDivElement>('view', location, setActiveSection);
|
||||
const customViewsRef = useScrollIntoView<HTMLDivElement>('custom-views', location, setActiveSection);
|
||||
const portRef = useScrollIntoView<HTMLDivElement>('port', location, setActiveSection);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -13,6 +13,7 @@ import Tag from '../../../../common/components/tag/Tag';
|
||||
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import { useSettingsDirty } from '../../useSettingsDirty';
|
||||
import CodeEditorModal from './composite/StyleEditorModal';
|
||||
|
||||
const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/';
|
||||
@@ -35,6 +36,7 @@ export default function ViewSettings() {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
useSettingsDirty(isDirty);
|
||||
|
||||
// update form if we get new data from server
|
||||
useEffect(() => {
|
||||
|
||||
@@ -86,6 +86,15 @@ const staticOptions = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function getPanelLabel(panel: string, location?: string): { panelLabel: string; sectionLabel?: string } {
|
||||
const panelOption = staticOptions.find((o) => o.id === panel);
|
||||
if (!panelOption) return { panelLabel: panel };
|
||||
const panelLabel = panelOption.label;
|
||||
if (!location || !('secondary' in panelOption)) return { panelLabel };
|
||||
const match = panelOption.secondary.find((s) => s.id.split('__')[1] === location);
|
||||
return { panelLabel, sectionLabel: match?.label };
|
||||
}
|
||||
|
||||
// a child of navigation or a child of secondary navigation
|
||||
export type SettingsOptionId =
|
||||
| (typeof staticOptions)[number]['id']
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { markClean, markDirty } from './appSettingsDirtyState';
|
||||
|
||||
export function useSettingsDirty(isDirty: boolean) {
|
||||
useEffect(() => {
|
||||
if (isDirty) {
|
||||
markDirty();
|
||||
} else {
|
||||
markClean();
|
||||
}
|
||||
return () => markClean();
|
||||
}, [isDirty]);
|
||||
}
|
||||
Reference in New Issue
Block a user