Ninth plugin out, third resurrection: switched off since 2026-08-13 with the permission reduced to api: [], so the server booted clean and every panel request 404'd. invoiceshelf/schema was also commented out of the drizzle barrel. Named `invoices`, not `invoiceshelf`. The appName drives the API prefix, the frontend route, the table prefix and the install key all at once, and before extraction those had drifted: the API was /api/invoiceshelf while the screen was /invoices. The URL is what a person sees and should name what the plugin shows rather than the daemon behind it — the permission key and label were already `invoices`, so the API prefix was the odd one out. Cost: three call sites, and renaming invoiceshelf_accounts to invoices_accounts to satisfy the table-prefix rule. That rename was free ONLY because the table existed on no server, which was checked before deciding. With rows in it, keeping the vendor name would have been the better trade: db:push sees a rename as a drop plus a create and this project has no migration mechanism. The sidecar keeps its Invoiceshelf-named functions deliberately — they name the upstream API it actually speaks. 91 palette classes converted, 52 of them in STATUS_TONE, which was already the single source of status colour but held raw palette strings. SENT and VIEWED now share `info`: the closed six-tone set has no second shade of "in flight, not yet actioned". Recorded in PLUGIN.md as a real loss.
89 lines
3.5 KiB
TypeScript
89 lines
3.5 KiB
TypeScript
import { Link } from 'react-router';
|
|
import { Check, ChevronsUpDown, Loader2, Settings2 } from 'lucide-react';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import { invoicesSectionPath } from './shared';
|
|
import { useInvoiceShelfAccountActions, useInvoiceShelfAccounts, useInvoiceShelfHealth } from './useInvoiceShelfData';
|
|
|
|
// Which books you are looking at, and how to change them.
|
|
//
|
|
// The nav header's subtitle was already the connection state, so the switcher takes that line rather than
|
|
// adding a second control saying nearly the same thing. With one account it stays exactly what it was: a
|
|
// line of text plus a way into the settings screen.
|
|
//
|
|
// Each entry shows its company under the label, because two accounts on the same instance differing only by
|
|
// company is the normal case here — the label alone would not tell them apart.
|
|
//
|
|
// Switching invalidates the whole ['invoiceshelf'] key: it changes the answer to every query in the
|
|
// workspace without changing any of their inputs.
|
|
|
|
export const AccountSwitcher = () => {
|
|
const { data } = useInvoiceShelfAccounts();
|
|
const { data: health } = useInvoiceShelfHealth();
|
|
const { activate } = useInvoiceShelfAccountActions();
|
|
|
|
const accounts = data?.accounts ?? [];
|
|
const active = accounts.find((account) => account.isActive) ?? null;
|
|
|
|
// Before the registry answers, fall back to health — it is the query that was already driving this line.
|
|
const status = active?.label ?? (health?.ok ? (health.version ?? 'connected') : 'not connected');
|
|
|
|
if (accounts.length < 2) {
|
|
return (
|
|
<Link
|
|
to={invoicesSectionPath('settings')}
|
|
className="block truncate text-xs text-muted-foreground hover:text-foreground"
|
|
>
|
|
{status}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger className="flex w-full items-center gap-1 text-xs text-muted-foreground hover:text-foreground">
|
|
<span className="truncate">{status}</span>
|
|
{activate.isPending ? (
|
|
<Loader2 className="h-3 w-3 shrink-0 animate-spin" />
|
|
) : (
|
|
<ChevronsUpDown className="h-3 w-3 shrink-0" />
|
|
)}
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-64">
|
|
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
|
InvoiceShelf accounts
|
|
</DropdownMenuLabel>
|
|
{accounts.map((account) => (
|
|
<DropdownMenuItem
|
|
key={account.id}
|
|
disabled={account.isActive || activate.isPending}
|
|
onSelect={() => void activate.mutateAsync(account.id).catch(() => undefined)}
|
|
className="gap-2"
|
|
>
|
|
<Check className={`h-3.5 w-3.5 shrink-0 ${account.isActive ? 'opacity-100' : 'opacity-0'}`} />
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block truncate">{account.label}</span>
|
|
<span className="block truncate text-[11px] text-muted-foreground">
|
|
{account.companyName ?? account.url}
|
|
</span>
|
|
</span>
|
|
</DropdownMenuItem>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem asChild>
|
|
<Link to={invoicesSectionPath('settings')} className="gap-2">
|
|
<Settings2 className="h-3.5 w-3.5" />
|
|
Manage accounts
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
};
|