a sweep for remnants after 2026-08-18. nothing was broken - no dangling import, symbol, route or permission key anywhere in src/ - but the platform was still speaking the plugin's vocabulary in a dozen places. the one that mattered is live code rather than prose: ChatPanelWrapper hardcodes workspace.id === 'email' to send context: 'email', which is the frontend half of the pair whose backend half (resolveEmailCwd) carries a fifteen-line justification. the two ends now both say so, including that adding a SECOND id there is the signal the platform needs a real way for a plugin to declare a chat context. the rest: two dead imports left by removing the settings section, an empty 'Email' type-section header, a protocol.ts section header with no members naming the plugin's SSE design, and a queue comment pointing at sidecar/email/sync-runner.ts - a path in another repository now. CLAUDE.md, AGENTS.md and working-on-officer.md described email as a platform feature, an app permission and a pm2 process. those are the files a fresh agent reads first, so they were the highest-risk staleness in the tree. TODO.md's whole Email section moved to the plugin's own repository. it held OPEN items pointing at sidecar/email/store.ts, which is exactly how a leftover list sends someone to edit files that do not exist. the delete-account dialog promised it would remove email accounts and playlists. both tables belong to plugins now, so that cascade only happens where the plugin is installed - it now says 'anything an installed plugin keeps for them' instead of naming two it cannot guarantee. imapflow and mailparser STAY in package.json. the plugin has no package.json of its own and resolves from the platform's node_modules, so removing them would break the installed plugin's sidecar. the fix is giving plugins their own dependency manifest, not deleting these. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
147 lines
5.3 KiB
TypeScript
147 lines
5.3 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 { BrowserRelay } from './BrowserRelay';
|
|
import { ApifyConfig } from './ApifyConfig';
|
|
import { DavAppPasswords } from './DavAppPasswords';
|
|
import { ApiKeys } from './ApiKeys';
|
|
|
|
const BASE_PATH = '/settings/integrations';
|
|
|
|
const enterpriseSections: SettingsSection[] = [
|
|
{
|
|
key: 'google-oauth',
|
|
icon: KeyRound,
|
|
title: 'Google OAuth',
|
|
description: 'Client ID and secret for Google APIs',
|
|
content: <GoogleOAuthConfig />,
|
|
},
|
|
{
|
|
key: 'apify',
|
|
icon: Wrench,
|
|
title: 'Apify',
|
|
description: 'API token for web scraping actors',
|
|
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',
|
|
// description: 'Connect your Google account',
|
|
// content: <GoogleAccount />,
|
|
// },
|
|
{
|
|
key: 'dav-app-passwords',
|
|
icon: CalendarDays,
|
|
title: 'Calendar & Contacts sync',
|
|
description: 'Per-device passwords for CalDAV/CardDAV clients',
|
|
content: <DavAppPasswords />,
|
|
},
|
|
{
|
|
key: 'api-keys',
|
|
icon: KeyRound,
|
|
title: 'API keys',
|
|
description: 'Per-app keys that sign in as you, revocable one at a time',
|
|
content: <ApiKeys />,
|
|
},
|
|
{
|
|
key: 'browser-relay',
|
|
icon: Globe,
|
|
title: 'Browser Relay',
|
|
description: 'Connect your Chrome browser',
|
|
content: <BrowserRelay />,
|
|
},
|
|
];
|
|
|
|
// 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 { 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" />
|
|
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">
|
|
Enterprise
|
|
</TabsTrigger>
|
|
<TabsTrigger value="personal" className="flex-1 cursor-pointer">
|
|
Personal
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</Tabs>
|
|
</div>
|
|
<SettingsSidebar basePath={BASE_PATH} icon={Puzzle} label="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>
|
|
);
|
|
};
|