SettingsSection now carries titleKey/descriptionKey instead of title/description, and the sidebar and content pane resolve them. The section arrays are module-level consts holding JSX content, so they cannot call a hook — the data holds keys and the renderers translate. Sidebar search matches the RESOLVED words, so it works in the language on screen. Capital K in titleKey is load-bearing: docs-check-settings.ts greps key:\s*'…' for route segments, and a lowercase title_key would have fed it twenty bogus paths. Verified 0 bogus, 26 declared, 20 offered, 0 missing. Section titles now share one key with the Spotlight rows that name the same section, per 'one key per meaning'. Two casings had to converge, so Settings shows Spotlight's: 'Change Password' now reads 'Change password'. Settings sidebar search placeholder was 'Search...'; it is now 'Search settings…', which is what the skeleton catalog already said and what distinguishes it from the global Spotlight.
139 lines
5.2 KiB
TypeScript
139 lines
5.2 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useNavigate, useParams } from 'react-router';
|
|
import { Puzzle, KeyRound, Globe, Wrench, CalendarDays } from 'lucide-react';
|
|
import type { LayoutNode, PanelComponents } from 'officerdev';
|
|
import { WorkspaceLayout } from 'officerdev';
|
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
|
|
import { SettingsRoute, SettingsSidebar, SettingsContent, type SettingsSection } from '../SettingsPanel';
|
|
import { GoogleOAuthConfig } from './GoogleOAuthConfig';
|
|
// import { GoogleAccount } from './GoogleAccount'; // hidden — email uses IMAP app password now
|
|
import { ApifyConfig } from './ApifyConfig';
|
|
import { DavAppPasswords } from './DavAppPasswords';
|
|
import { useTranslation } from '@/lib/i18n';
|
|
|
|
const BASE_PATH = '/settings/integrations';
|
|
|
|
const enterpriseSections: SettingsSection[] = [
|
|
{
|
|
key: 'google-oauth',
|
|
icon: KeyRound,
|
|
titleKey: 'settings.sections.googleOauth.label',
|
|
descriptionKey: 'settings.sections.googleOauth.description',
|
|
content: <GoogleOAuthConfig />,
|
|
},
|
|
{
|
|
key: 'apify',
|
|
icon: Wrench,
|
|
titleKey: 'settings.sections.apify.label',
|
|
descriptionKey: 'settings.sections.apify.description',
|
|
content: <ApifyConfig />,
|
|
},
|
|
];
|
|
|
|
const personalSections: SettingsSection[] = [
|
|
// Email account management left with the feature on 2026-08-18 (`plugins/email/`). A plugin contributes
|
|
// panels, not settings sections, so it now lives inside the email workspace itself.
|
|
// Google OAuth is left in place (code + DB) but hidden — email now uses an app password over IMAP,
|
|
// so there's no reason to reconnect Google here. Re-enable this section to bring it back.
|
|
// {
|
|
// key: 'google-account',
|
|
// icon: UserCircle,
|
|
// title: 'Google Account',
|
|
// descriptionKey: 'settings.sections.apify.description',
|
|
// content: <GoogleAccount />,
|
|
// },
|
|
{
|
|
key: 'dav-app-passwords',
|
|
icon: CalendarDays,
|
|
titleKey: 'settings.sections.davSync.label',
|
|
descriptionKey: 'settings.sections.davSync.description',
|
|
content: <DavAppPasswords />,
|
|
},
|
|
];
|
|
|
|
// 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/apify` 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 { t } = useTranslation();
|
|
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">
|
|
<div className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium bg-duck-teal/15 text-duck-teal">
|
|
<Puzzle className="h-4 w-4" />
|
|
{t('settings.nav.integrations')}
|
|
</div>
|
|
</div>
|
|
<div className="px-3 pb-2">
|
|
<Tabs value={tab} onValueChange={setTab}>
|
|
<TabsList className="w-full">
|
|
<TabsTrigger value="enterprise" className="flex-1 cursor-pointer">
|
|
{t('settings.integrations.enterprise')}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="personal" className="flex-1 cursor-pointer">
|
|
{t('settings.integrations.personal')}
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</Tabs>
|
|
</div>
|
|
<SettingsSidebar
|
|
basePath={BASE_PATH}
|
|
icon={Puzzle}
|
|
labelKey="settings.nav.integrations"
|
|
sections={sections}
|
|
hideHeader
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 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',
|
|
id: 'integrations-root',
|
|
direction: 'horizontal',
|
|
children: [
|
|
{ node: { type: 'panel', id: 'integrations-left', appType: null }, size: 20 },
|
|
{ node: { type: 'panel', id: 'integrations-right', appType: null }, size: 80 },
|
|
],
|
|
};
|
|
|
|
export const IntegrationsSettings = () => {
|
|
const panelComponents: PanelComponents = useMemo(
|
|
() => ({
|
|
'integrations-left': IntegrationsSidebar,
|
|
'integrations-right': IntegrationsContent,
|
|
}),
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<SettingsRoute basePath={BASE_PATH} sections={allSections}>
|
|
<div className="h-full w-full pt-2">
|
|
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
|
|
</div>
|
|
</SettingsRoute>
|
|
);
|
|
};
|