Settings → Integrations → Calendar & Contacts sync. without this there is no way to mint a credential from the app, and minting one was the first step of testing the whole caldav feature on a phone. the generated password is returned by POST and never again — it is stored as an argon2 hash, so there is nothing to read back. that one fact drives the screen: the new credential appears in a panel that stays until dismissed, with the server url and username beside it, because once it is gone the only remedy is to revoke and mint another. the server url is read from window.location.origin rather than configured. it is by definition the address that reached this page, so it is the one that will work on the phone. rows show the hint prefix and last-used date. "never used" is the tell that a device was set up wrong, so it gets its own wording rather than a blank. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
import { useMemo } from 'react';
|
|
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 { GoogleOAuthConfig } from './GoogleOAuthConfig';
|
|
// import { GoogleAccount } from './GoogleAccount'; // hidden — email uses IMAP app password now
|
|
import { BrowserRelay } from './BrowserRelay';
|
|
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 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[] = [
|
|
{
|
|
key: 'email-accounts',
|
|
icon: Mail,
|
|
title: 'Email',
|
|
description: 'Connect email accounts for inbox sync',
|
|
content: <EmailAccounts />,
|
|
},
|
|
// 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: 'browser-relay',
|
|
icon: Globe,
|
|
title: 'Browser Relay',
|
|
description: 'Connect your Chrome browser',
|
|
content: <BrowserRelay />,
|
|
},
|
|
];
|
|
|
|
const IntegrationsSidebar = () => {
|
|
const [tab, setTab] = useGlobal<string>(TAB_KEY, 'enterprise');
|
|
const sections = tab === 'enterprise' ? enterpriseSections : personalSections;
|
|
|
|
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 globalKey={GLOBAL_KEY} 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} />;
|
|
};
|
|
|
|
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 (
|
|
<div className="h-full w-full pt-2">
|
|
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
|
|
</div>
|
|
);
|
|
};
|