feat(accounts): remove a specific account from the switcher
The switcher only offered 'sign out of active' and 'sign out of all' — no way to drop a single non-active account (e.g. one stuck in an error state you can't switch into to sign out). Add a hover × on non-active, non-default rows and a removeAccount(id) auth action that tears down the client, drops it from the registry, and clears its per-slot session/token cookies. Stacks on the switcher redesign in #517.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Check, Plus, LogOut, Star, ChevronDown, AlertCircle, GripVertical } from "lucide-react";
|
||||
import { Check, Plus, LogOut, Star, ChevronDown, AlertCircle, GripVertical, X } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useAccountStore, type AccountEntry } from "@/stores/account-store";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
@@ -48,6 +48,7 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher
|
||||
const activeAccount = accounts.find((a) => a.id === activeAccountId);
|
||||
const switchAccount = useAuthStore((s) => s.switchAccount);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
const removeAccount = useAuthStore((s) => s.removeAccount);
|
||||
const logoutAll = useAuthStore((s) => s.logoutAll);
|
||||
|
||||
const updatePosition = useCallback(() => {
|
||||
@@ -100,6 +101,13 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher
|
||||
router.push(`/login?mode=add-account` as never);
|
||||
};
|
||||
|
||||
const handleRemove = (e: React.MouseEvent, account: AccountEntry) => {
|
||||
e.stopPropagation();
|
||||
const label = account.email || account.username;
|
||||
if (!window.confirm(t("remove_account_confirm", { account: label }))) return;
|
||||
removeAccount(account.id);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
setOpen(false);
|
||||
logout();
|
||||
@@ -221,7 +229,7 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher
|
||||
className={cn(
|
||||
"w-full flex items-start gap-3 px-3 py-2.5 text-start transition-colors",
|
||||
isActive ? "bg-accent/50" : "hover:bg-muted",
|
||||
isDraggable && "pe-7"
|
||||
(!isActive && !account.isDefault) ? (isDraggable ? "pe-14" : "pe-8") : (isDraggable && "pe-7")
|
||||
)}
|
||||
role="menuitem"
|
||||
disabled={isActive}
|
||||
@@ -264,11 +272,22 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher
|
||||
{isDraggable && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute end-2 top-1/2 -translate-y-1/2 text-muted-foreground/50 opacity-0 transition-opacity group-hover/acct:opacity-100"
|
||||
className="pointer-events-none absolute end-7 top-1/2 -translate-y-1/2 text-muted-foreground/50 opacity-0 transition-opacity group-hover/acct:opacity-100"
|
||||
>
|
||||
<GripVertical className="w-4 h-4" />
|
||||
</span>
|
||||
)}
|
||||
{!isActive && !account.isDefault && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleRemove(e, account)}
|
||||
aria-label={t("remove_account")}
|
||||
title={t("remove_account")}
|
||||
className="absolute end-1.5 top-1/2 -translate-y-1/2 p-1 rounded-md text-muted-foreground/60 opacity-0 transition-opacity group-hover/acct:opacity-100 hover:bg-destructive/10 hover:text-destructive focus:opacity-100 focus:outline-none focus:ring-1 focus:ring-destructive"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Naplánováno",
|
||||
"unified_all_unread": "Vše nepřečtené",
|
||||
"unified_all_starred": "Vše s hvězdičkou",
|
||||
"unified_all_mail": "Veškerá pošta"
|
||||
"unified_all_mail": "Veškerá pošta",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Výchozí aplikace",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Planlagt",
|
||||
"unified_all_unread": "Alle ulæste",
|
||||
"unified_all_starred": "Alle med stjerne",
|
||||
"unified_all_mail": "Al post"
|
||||
"unified_all_mail": "Al post",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Standardapps",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Geplant",
|
||||
"unified_all_unread": "Alle ungelesenen",
|
||||
"unified_all_starred": "Alle markierten",
|
||||
"unified_all_mail": "Alle Mails"
|
||||
"unified_all_mail": "Alle Mails",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Standard-Apps",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Scheduled",
|
||||
"unified_all_unread": "All unread",
|
||||
"unified_all_starred": "All starred",
|
||||
"unified_all_mail": "All mail"
|
||||
"unified_all_mail": "All mail",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Default apps",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Programados",
|
||||
"unified_all_unread": "Todo no leído",
|
||||
"unified_all_starred": "Todo destacado",
|
||||
"unified_all_mail": "Todo el correo"
|
||||
"unified_all_mail": "Todo el correo",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Aplicaciones predeterminadas",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "زمانبندی شده",
|
||||
"unified_all_mail": "همه ایمیلها",
|
||||
"unified_all_starred": "همه ستارهدارها",
|
||||
"unified_all_unread": "همه خواندهنشدهها"
|
||||
"unified_all_unread": "همه خواندهنشدهها",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "برنامههای پیشفرض",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Planifiés",
|
||||
"unified_all_unread": "Tous les non lus",
|
||||
"unified_all_starred": "Tous les favoris",
|
||||
"unified_all_mail": "Tout le courrier"
|
||||
"unified_all_mail": "Tout le courrier",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Applications par défaut",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Ütemezett",
|
||||
"unified_all_unread": "Összes olvasatlan",
|
||||
"unified_all_starred": "Összes csillagozott",
|
||||
"unified_all_mail": "Összes levél"
|
||||
"unified_all_mail": "Összes levél",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Alapértelmezett alkalmazások",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Programmate",
|
||||
"unified_all_unread": "Tutti i non letti",
|
||||
"unified_all_starred": "Tutti gli speciali",
|
||||
"unified_all_mail": "Tutta la posta"
|
||||
"unified_all_mail": "Tutta la posta",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "App predefinite",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "予約済み",
|
||||
"unified_all_unread": "すべての未読",
|
||||
"unified_all_starred": "すべてのスター付き",
|
||||
"unified_all_mail": "すべてのメール"
|
||||
"unified_all_mail": "すべてのメール",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "既定のアプリ",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "예약됨",
|
||||
"unified_all_unread": "모든 읽지 않음",
|
||||
"unified_all_starred": "모든 별표",
|
||||
"unified_all_mail": "모든 메일"
|
||||
"unified_all_mail": "모든 메일",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "기본 앱",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Ieplānots",
|
||||
"unified_all_unread": "Visi nelasītie",
|
||||
"unified_all_starred": "Visi ar zvaigzni",
|
||||
"unified_all_mail": "Visas vēstules"
|
||||
"unified_all_mail": "Visas vēstules",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Noklusējuma lietotnes",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Gepland",
|
||||
"unified_all_unread": "Alle ongelezen",
|
||||
"unified_all_starred": "Alle met ster",
|
||||
"unified_all_mail": "Alle e-mail"
|
||||
"unified_all_mail": "Alle e-mail",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Standaardapps",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Zaplanowane",
|
||||
"unified_all_unread": "Wszystkie nieprzeczytane",
|
||||
"unified_all_starred": "Wszystkie oznaczone gwiazdką",
|
||||
"unified_all_mail": "Cała poczta"
|
||||
"unified_all_mail": "Cała poczta",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Aplikacje domyślne",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Agendados",
|
||||
"unified_all_unread": "Tudo não lido",
|
||||
"unified_all_starred": "Tudo com estrela",
|
||||
"unified_all_mail": "Todo o correio"
|
||||
"unified_all_mail": "Todo o correio",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Aplicativos padrão",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Programat",
|
||||
"unified_all_unread": "Toate necitite",
|
||||
"unified_all_starred": "Toate cu stea",
|
||||
"unified_all_mail": "Toată poșta"
|
||||
"unified_all_mail": "Toată poșta",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Aplicații implicite",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Запланировано",
|
||||
"unified_all_unread": "Все непрочитанные",
|
||||
"unified_all_starred": "Все помеченные",
|
||||
"unified_all_mail": "Вся почта"
|
||||
"unified_all_mail": "Вся почта",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Приложения по умолчанию",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Zamanlandı",
|
||||
"unified_all_unread": "Tüm okunmamışlar",
|
||||
"unified_all_starred": "Tüm yıldızlılar",
|
||||
"unified_all_mail": "Tüm postalar"
|
||||
"unified_all_mail": "Tüm postalar",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Varsayılan uygulamalar",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "Заплановано",
|
||||
"unified_all_unread": "Усі непрочитані",
|
||||
"unified_all_starred": "Усі позначені",
|
||||
"unified_all_mail": "Уся пошта"
|
||||
"unified_all_mail": "Уся пошта",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "Програми за замовчуванням",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"scheduled": "已计划",
|
||||
"unified_all_unread": "全部未读",
|
||||
"unified_all_starred": "全部加星标",
|
||||
"unified_all_mail": "全部邮件"
|
||||
"unified_all_mail": "全部邮件",
|
||||
"remove_account": "Remove account",
|
||||
"remove_account_confirm": "Remove {account} from this device? You can add it back later."
|
||||
},
|
||||
"protocol_handlers": {
|
||||
"title": "默认应用",
|
||||
|
||||
@@ -44,6 +44,7 @@ interface AuthState {
|
||||
refreshAccessToken: () => Promise<string | null>;
|
||||
logout: () => void;
|
||||
logoutAll: () => void;
|
||||
removeAccount: (accountId: string) => void;
|
||||
switchAccount: (accountId: string) => Promise<void>;
|
||||
checkAuth: () => Promise<void>;
|
||||
clearError: () => void;
|
||||
@@ -1243,6 +1244,31 @@ export const useAuthStore = create<AuthState>()(
|
||||
redirectToLogin();
|
||||
},
|
||||
|
||||
// Remove a specific (typically non-active) account: tear down its client,
|
||||
// drop it from the registry, and clear its per-slot cookies. If asked to
|
||||
// remove the active account, defer to logout() which handles switching
|
||||
// away or redirecting.
|
||||
removeAccount: (accountId: string) => {
|
||||
if (accountId === get().activeAccountId) { get().logout(); return; }
|
||||
const accountStore = useAccountStore.getState();
|
||||
const account = accountStore.getAccountById(accountId);
|
||||
if (!account) return;
|
||||
const slot = account.cookieSlot ?? 0;
|
||||
const wasOAuth = account.authMode === 'oauth';
|
||||
|
||||
clearRefreshTimer(accountId);
|
||||
const client = clients.get(accountId);
|
||||
if (client) { try { client.disconnect(); } catch { /* noop */ } }
|
||||
clients.delete(accountId);
|
||||
evictAccount(accountId);
|
||||
accountStore.removeAccount(accountId);
|
||||
|
||||
apiFetch(`/api/auth/session?slot=${slot}`, { method: 'DELETE', keepalive: true }).catch(() => {});
|
||||
if (wasOAuth) {
|
||||
apiFetch(`/api/auth/token?slot=${slot}`, { method: 'DELETE', keepalive: true }).catch(() => {});
|
||||
}
|
||||
},
|
||||
|
||||
logoutAll: () => {
|
||||
// Disconnect all clients
|
||||
for (const c of clients.values()) {
|
||||
|
||||
Reference in New Issue
Block a user