import { useState } from 'react' import { AlertTriangle, Check, ChevronDown, Clock, Copy, FolderOpen, KeyRound, Link2Off, RefreshCw, } from 'lucide-react' import ConfirmDialog from '../../shared/ConfirmDialog' import CustomSelect from '../../shared/CustomSelect' import ToggleSwitch from '../../Settings/ToggleSwitch' import Tooltip from '../../shared/Tooltip' import { useTranslation } from '../../../i18n/TranslationContext' import { DOCUMENT_PROVIDER_ICONS } from '../../shared/documentProviderIconMap' import DocSyncFlow, { type SyncDirection } from './DocSyncFlow' import { Badge, CONFLICT_POLICIES, conflictPolicyKey, LastRun, StateBadge } from './DocSyncBits' import { bindingNotice, needsReauth, type DocSyncLink, type useDocSync } from './useDocSync' /** * One binding: where this trip's documents live, which way they move, and what * happened last time. * * The flow bar carries the direction because that is the one thing this feature * is about. Everything a person sets once and forgets (the delete rule, the * webhook URL) is folded away behind "Settings" so the card stays a status * card rather than a form. */ export default function DocSyncBinding({ link, providerName, sync, canManage, onReconnect, onEditConnection, }: { link: DocSyncLink providerName: string sync: ReturnType canManage: boolean /** * Opens the credential form for this binding's store. Only the panel can, * because the form needs the provider's field list, which the card does * not carry; left out, a refused credential is reported but not curable. */ onReconnect?: () => void /** The same form at any time, to change the address or the login, or to remove the connection. */ onEditConnection?: () => void }) { const { t } = useTranslation() const [open, setOpen] = useState(false) const [copied, setCopied] = useState(false) const [confirmUnlink, setConfirmUnlink] = useState(false) const Icon = DOCUMENT_PROVIDER_ICONS[link.providerId] const busy = sync.busy === `sync-${link.id}` const notice = bindingNotice(link, t) // Not while the provider is switched off: the paused notice stands in front // of the refusal then, and a new credential would change nothing until an // admin turns the provider back on. const reconnect = canManage && onReconnect && !link.providerOff && needsReauth(link) ? onReconnect : null const copyWebhook = async () => { if (!link.webhookUrl) return try { await navigator.clipboard.writeText(link.webhookUrl) setCopied(true) setTimeout(() => setCopied(false), 1600) } catch { // Clipboard is blocked outside a secure context; the URL is selectable. } } return (
{Icon ? : null} {providerName} {/* A healthy binding needs no word for it: the dot is the whole message, and the states that do need words get them below. */} {!link.syncEnabled && {t('docsync.binding.autoOff')}} {/* Where and when, as two facts rather than a sentence. They are what a member checks before asking why a file has not turned up. */} } title={link.remoteRootPath || undefined}> {link.remoteLabel || link.remoteRootPath || link.scopeKey} }> {/* Both controls are h-9 so the square one cannot sit a pixel off the other: the two together read as one segmented control. */} {/* Asked first, as the phone does: the button sits a hand's width from "Sync now", and a binding cannot be put back. Rebinding the same folder starts from nothing and copies both sides over again. */} {canManage && ( )}
{notice && (

{notice} {/* The one state a person can act on from here. Without this the only way past a rotated token was the API: the sidebar opens the form for a store that has no connection yet, and this one has. */} {reconnect && ( )}

)} setConfirmUnlink(false)} onConfirm={() => void sync.removeLink(link.id)} title={t('docsync.unlink')} message={t('docsync.confirmUnlink')} confirmLabel={t('docsync.unlink')} />
void sync.updateLink(link.id, { direction: next })} />
{canManage && ( <> {open && (
void sync.updateLink(link.id, { deletePolicy: String(v) })} options={[ { value: 'unlink', label: t('docsync.deleteUnlink') }, { value: 'trash', label: t('docsync.deleteTrash') }, ]} /> void sync.updateLink(link.id, { conflictPolicy: String(v) })} options={CONFLICT_POLICIES.map(p => ({ value: p, label: t(conflictPolicyKey(p)) }))} /> void sync.updateLink(link.id, { syncEnabled: !link.syncEnabled })} /> {/* Only where TREK could not subscribe itself. Papra's webhook CRUD is closed to API keys and Nextcloud's needs admin rights, so the person pastes this in by hand; polling carries it either way. */} {link.webhookUrl && (
{/* Same tier as the two rows above it: this is the third setting, not a footnote to the second. */} {t('docsync.binding.webhookTitle')}

{t('docsync.webhookHint')}

{link.webhookUrl}
)} {onEditConnection && ( )}
)} )}
) } function Row({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { return (
{label} {hint && {hint}} {children}
) }