import { useId, useState } from 'react' import { AlertTriangle, Check, Loader2, Plug, ShieldAlert } from 'lucide-react' import { DialogButton, DialogFooter, DialogHeader, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT } from '../../shared/DialogShell' import { Tooltip } from '../../shared/Tooltip' import ToggleSwitch from '../../Settings/ToggleSwitch' import { useTranslation } from '../../../i18n/TranslationContext' import { useConnectForm } from './useConnectForm' import { Badge } from './DocSyncBits' import { DOCUMENT_PROVIDER_ICONS } from '../../shared/DocumentProviderIcons' import type { DocSyncProvider, DocSyncVerdict, useDocSync } from './useDocSync' /** * Connecting a store, as a dialog rather than a panel that unfolds in place. * * Entering a URL and a token is a task with a beginning and an end, and doing it * inside the list meant the list jumped around while someone typed. The dialog * also gives the probe result somewhere to live that is not a line of text * squeezed between two fields. */ export default function DocSyncConnectModal({ provider, sync, onClose, onConnected, }: { provider: DocSyncProvider sync: ReturnType onClose: () => void onConnected: (providerId: string) => void }) { const { t } = useTranslation() const existing = sync.connectionFor(provider.id) const Icon = DOCUMENT_PROVIDER_ICONS[provider.id] const form = useConnectForm(provider, existing, sync) const [verdict, setVerdict] = useState(null) const test = async () => setVerdict(await form.probe()) const labelId = useId() const save = async () => { if (await form.save()) onConnected(provider.id) } return ( {Icon ? : }} tint={NEUTRAL_TINT} labelId={labelId} onClose={onClose} title={provider.name} /> )} footer={( void test()} disabled={!form.complete || sync.busy === 'test'}> {t('docsync.test')} void save()} disabled={!form.complete || sync.busy === 'save'} icon={sync.busy === 'save' ? : undefined}> {t('docsync.connect.submit')} )} >

{t(`docsync.connect.about.${provider.id}`)}

{form.fields.map(f => ( form.setValue(f.field_key, e.target.value)} className="w-full rounded-lg border border-edge bg-surface-input px-3 py-2.5 text-body text-content ring-accent transition-shadow placeholder:text-content-faint focus:outline-none focus:ring-2" /> ))}
) } function Field({ label, hint, required, children, }: { label: string hint?: string required?: boolean children: React.ReactNode }) { const { t } = useTranslation() return ( ) } /** The probe result, in the footer where it stays put while the form scrolls. */ function Verdict({ verdict, busy, failure, }: { verdict: DocSyncVerdict | null busy: boolean /** A call that failed outside the connection test (saving, most of all). */ failure: string | null }) { const { t } = useTranslation() if (busy) { return ( {t('docsync.connect.testing')} ) } // A save that the server refused left the dialog open with the button back to // normal and nothing else said, which reads as nothing having happened. The // test verdict wins the space when there is one: it is the more specific // answer and the one the person just asked for. if (!verdict && failure) { return ( {t(`docsync.error.${failure}`)} ) } if (!verdict) return if (verdict.connected) { return ( {verdict.account ? t('docsync.connect.okAs', { account: verdict.account }) : t('docsync.connected')} ) } const blocked = verdict.error === 'ssrf_blocked' || verdict.error === 'tls_untrusted' const message = t(`docsync.error.${verdict.error || 'unknown'}`) // The footer shares its row with the buttons, and a refusal cut to "The // credentials were re..." with no way to read the rest left nothing to go on // (#2666). Two lines, and the whole of it with the store's reason on hover. return ( {blocked ? : } {message} ) }