put the settings section in the url

Five settings pages moved from a `*_SELECTED` global to `/settings/:page/:section`. The
sidebar entry is a react-router `<NavLink>` rather than a button holding the key in its
onClick closure, so a section is linkable, cmd-clickable and gets its active state from the
router; each page renders one `SettingsRoute` guard that canonicalises both the bare route
and a section that does not exist.

Integrations needed more than the shared factory. It builds its own sidebar, and it kept the
Enterprise/Personal tab in a second global — which is why a deep link to a Personal section
could never have worked: the link set the section, the tab stayed on Enterprise, and the
content pane said "Select a section" about a section that existed. The tab is derived from
the section key now.

Also removes the `/settings/resources` menu item (audit M8) and its two locale keys: there
has never been such a route, so it bounced to the catch-all and out to `/`.
This commit is contained in:
2026-08-07 11:38:21 +00:00
parent 98ba61f135
commit 2502c33804
12 changed files with 155 additions and 99 deletions
@@ -1,12 +1,12 @@
import { useMemo } from 'react';
import { useNavigate, useParams } from 'react-router';
import { Puzzle, KeyRound, UserCircle, Globe, Wrench, Mail, CalendarDays } from 'lucide-react';
import type { LayoutNode, PanelComponents } from 'officerdev';
import { WorkspaceLayout } from 'officerdev';
import { useAuth } from 'hooks/useAuth';
import { useGlobal } from 'hooks/useGlobal';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { SettingsSidebar, SettingsContent, type SettingsSection } from '../SettingsPanel';
import { SettingsRoute, SettingsSidebar, SettingsContent, type SettingsSection } from '../SettingsPanel';
import { GoogleOAuthConfig } from './GoogleOAuthConfig';
// import { GoogleAccount } from './GoogleAccount'; // hidden — email uses IMAP app password now
import { BrowserRelay } from './BrowserRelay';
@@ -14,8 +14,7 @@ import { ApifyConfig } from './ApifyConfig';
import { EmailAccounts } from './EmailAccounts';
import { DavAppPasswords } from './DavAppPasswords';
const GLOBAL_KEY = 'INTEGRATIONS_SETTINGS_SELECTED';
const TAB_KEY = 'INTEGRATIONS_SETTINGS_TAB';
const BASE_PATH = '/settings/integrations';
const enterpriseSections: SettingsSection[] = [
{
@@ -67,10 +66,27 @@ const personalSections: SettingsSection[] = [
},
];
// The Enterprise/Personal tab is not a second thing to remember — it is a fact about which section is
// open, so it is derived from the URL rather than kept in its own global. That also fixes the deep link:
// with the tab held separately, `/settings/integrations/email-accounts` would have arrived with the tab
// still on Enterprise and the content pane showing "Select a section".
const allSections = [...enterpriseSections, ...personalSections];
const tabForSection = (section: string | undefined) =>
personalSections.some((s) => s.key === section) ? 'personal' : 'enterprise';
const IntegrationsSidebar = () => {
const [tab, setTab] = useGlobal<string>(TAB_KEY, 'enterprise');
const { section } = useParams<{ section?: string }>();
const navigate = useNavigate();
const tab = tabForSection(section);
const sections = tab === 'enterprise' ? enterpriseSections : personalSections;
// Switching tab is a navigation, so it lands on that tab's first section rather than leaving the
// content pane showing a section the sidebar no longer lists.
const setTab = (next: string) => {
const first = (next === 'enterprise' ? enterpriseSections : personalSections)[0];
if (first) navigate(`${BASE_PATH}/${first.key}`);
};
return (
<div className="flex flex-col h-full">
<div className="p-3 pb-2">
@@ -91,17 +107,14 @@ const IntegrationsSidebar = () => {
</TabsList>
</Tabs>
</div>
<SettingsSidebar globalKey={GLOBAL_KEY} icon={Puzzle} label="Integrations" sections={sections} hideHeader />
<SettingsSidebar basePath={BASE_PATH} icon={Puzzle} label="Integrations" sections={sections} hideHeader />
</div>
);
};
const IntegrationsContent = () => {
const [tab] = useGlobal<string>(TAB_KEY, 'enterprise');
const sections = tab === 'enterprise' ? enterpriseSections : personalSections;
return <SettingsContent globalKey={GLOBAL_KEY} sections={sections} />;
};
// Both tabs' sections, because the URL alone says which one is open and the pane no longer has to agree
// with the sidebar about the tab to find it.
const IntegrationsContent = () => <SettingsContent sections={allSections} />;
const layout: LayoutNode = {
type: 'group',
@@ -123,8 +136,10 @@ export const IntegrationsSettings = () => {
);
return (
<div className="h-full w-full pt-2">
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
</div>
<SettingsRoute basePath={BASE_PATH} sections={allSections}>
<div className="h-full w-full pt-2">
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
</div>
</SettingsRoute>
);
};