(null);
+ const sort = controlledSort === undefined ? uncontrolledSort : controlledSort;
const sortedAccounts = useMemo(() => {
if (!sort) {
return accounts;
@@ -252,12 +261,13 @@ export function AccountList({ accounts, readOnly = false, onAction }: AccountLis
}, [accounts, sort]);
const handleSort = (key: AccountListSortKey) => {
- setSort((current) => {
- if (current?.key !== key) {
- return { key, direction: "asc" };
- }
- return { key, direction: current.direction === "asc" ? "desc" : "asc" };
- });
+ const nextSort: AccountListSort = sort?.key === key
+ ? { key, direction: sort.direction === "asc" ? "desc" : "asc" }
+ : { key, direction: "asc" };
+ if (controlledSort === undefined) {
+ setUncontrolledSort(nextSort);
+ }
+ onSortChange?.(nextSort);
};
if (accounts.length === 0) {
diff --git a/frontend/src/features/dashboard/components/dashboard-page.test.tsx b/frontend/src/features/dashboard/components/dashboard-page.test.tsx
index ff67e9048..caf33e7f1 100644
--- a/frontend/src/features/dashboard/components/dashboard-page.test.tsx
+++ b/frontend/src/features/dashboard/components/dashboard-page.test.tsx
@@ -43,9 +43,25 @@ vi.mock("@/features/dashboard/components/account-cards", () => ({
}));
vi.mock("@/features/dashboard/components/account-list", () => ({
- AccountList: ({ accounts }: { accounts: Array<{ accountId: string }> }) => {
- accountListSpy(accounts);
- return List for {accounts.length} accounts
;
+ AccountList: ({
+ accounts,
+ sort,
+ onSortChange,
+ }: {
+ accounts: Array<{ accountId: string }>;
+ sort: { key: string; direction: string } | null;
+ onSortChange: (sort: { key: string; direction: string }) => void;
+ }) => {
+ accountListSpy({ accounts, sort });
+ return (
+
+ );
},
}));
@@ -103,6 +119,7 @@ describe("DashboardPage", () => {
useDashboardPreferencesStore.setState({
accountBurnrateEnabled: true,
accountViewMode: "cards",
+ accountListSort: null,
initialized: true,
});
});
@@ -212,7 +229,30 @@ describe("DashboardPage", () => {
expect(screen.getByTestId("account-list")).toHaveTextContent("List for 2 accounts");
expect(screen.queryByTestId("account-cards")).not.toBeInTheDocument();
- expect(accountListSpy).toHaveBeenCalledWith(overview.accounts);
+ expect(accountListSpy).toHaveBeenCalledWith({ accounts: overview.accounts, sort: null });
expect(useDashboardPreferencesStore.getState().accountViewMode).toBe("list");
});
+
+ it("passes persisted account list sort through and updates it from the list", async () => {
+ const user = userEvent.setup();
+ const overview = mockReadyDashboard();
+ useDashboardPreferencesStore.setState({
+ accountBurnrateEnabled: true,
+ accountViewMode: "list",
+ accountListSort: { key: "quota", direction: "asc" },
+ initialized: true,
+ });
+
+ renderWithProviders();
+
+ expect(screen.getByTestId("account-list")).toHaveTextContent("List for 2 accounts");
+ expect(accountListSpy).toHaveBeenCalledWith({
+ accounts: overview.accounts,
+ sort: { key: "quota", direction: "asc" },
+ });
+
+ await user.click(screen.getByTestId("account-list"));
+
+ expect(useDashboardPreferencesStore.getState().accountListSort).toEqual({ key: "credits", direction: "desc" });
+ });
});
diff --git a/frontend/src/features/dashboard/components/dashboard-page.tsx b/frontend/src/features/dashboard/components/dashboard-page.tsx
index d5864f791..2e4d2a690 100644
--- a/frontend/src/features/dashboard/components/dashboard-page.tsx
+++ b/frontend/src/features/dashboard/components/dashboard-page.tsx
@@ -40,7 +40,9 @@ export function DashboardPage() {
const isDark = useThemeStore((s) => s.theme === "dark");
const showAccountBurnrate = useDashboardPreferencesStore((s) => s.accountBurnrateEnabled);
const accountViewMode = useDashboardPreferencesStore((s) => s.accountViewMode);
+ const accountListSort = useDashboardPreferencesStore((s) => s.accountListSort);
const setAccountViewMode = useDashboardPreferencesStore((s) => s.setAccountViewMode);
+ const setAccountListSort = useDashboardPreferencesStore((s) => s.setAccountListSort);
const canWrite = useAuthStore((state) => state.canWrite);
const overviewTimeframe = useMemo(
() => parseOverviewTimeframe(searchParams.get("overviewTimeframe")),
@@ -237,7 +239,13 @@ export function DashboardPage() {
{accountViewMode === "list" ? (
-
+
) : (
)}
diff --git a/frontend/src/hooks/use-dashboard-preferences.test.ts b/frontend/src/hooks/use-dashboard-preferences.test.ts
index 60ad91462..92d9fcdbb 100644
--- a/frontend/src/hooks/use-dashboard-preferences.test.ts
+++ b/frontend/src/hooks/use-dashboard-preferences.test.ts
@@ -31,7 +31,9 @@ describe("useDashboardPreferencesStore", () => {
useDashboardPreferencesStore.getState().initializePreferences();
expect(useDashboardPreferencesStore.getState().accountViewMode).toBe("cards");
+ expect(useDashboardPreferencesStore.getState().accountListSort).toBeNull();
expect(window.localStorage.getItem("codex-lb-dashboard-account-view-mode")).toBe("cards");
+ expect(window.localStorage.getItem("codex-lb-dashboard-account-list-sort")).toBeNull();
});
it("persists account view mode updates", async () => {
@@ -42,4 +44,43 @@ describe("useDashboardPreferencesStore", () => {
expect(useDashboardPreferencesStore.getState().accountViewMode).toBe("list");
expect(window.localStorage.getItem("codex-lb-dashboard-account-view-mode")).toBe("list");
});
+
+ it("persists account list sort updates", async () => {
+ const { useDashboardPreferencesStore } = await import("@/hooks/use-dashboard-preferences");
+
+ useDashboardPreferencesStore.getState().setAccountListSort({ key: "quota", direction: "asc" });
+
+ expect(useDashboardPreferencesStore.getState().accountListSort).toEqual({ key: "quota", direction: "asc" });
+ expect(window.localStorage.getItem("codex-lb-dashboard-account-list-sort")).toBe(
+ JSON.stringify({ key: "quota", direction: "asc" }),
+ );
+ });
+
+ it("restores stored account list sort on initialization", async () => {
+ window.localStorage.setItem(
+ "codex-lb-dashboard-account-list-sort",
+ JSON.stringify({ key: "credits", direction: "desc" }),
+ );
+ const { useDashboardPreferencesStore } = await import("@/hooks/use-dashboard-preferences");
+
+ useDashboardPreferencesStore.getState().initializePreferences();
+
+ expect(useDashboardPreferencesStore.getState().accountListSort).toEqual({ key: "credits", direction: "desc" });
+ expect(window.localStorage.getItem("codex-lb-dashboard-account-list-sort")).toBe(
+ JSON.stringify({ key: "credits", direction: "desc" }),
+ );
+ });
+
+ it("ignores invalid stored account list sort", async () => {
+ window.localStorage.setItem(
+ "codex-lb-dashboard-account-list-sort",
+ JSON.stringify({ key: "invalid", direction: "desc" }),
+ );
+ const { useDashboardPreferencesStore } = await import("@/hooks/use-dashboard-preferences");
+
+ useDashboardPreferencesStore.getState().initializePreferences();
+
+ expect(useDashboardPreferencesStore.getState().accountListSort).toBeNull();
+ expect(window.localStorage.getItem("codex-lb-dashboard-account-list-sort")).toBeNull();
+ });
});
diff --git a/frontend/src/hooks/use-dashboard-preferences.ts b/frontend/src/hooks/use-dashboard-preferences.ts
index 9d3c73fca..1d9782022 100644
--- a/frontend/src/hooks/use-dashboard-preferences.ts
+++ b/frontend/src/hooks/use-dashboard-preferences.ts
@@ -1,19 +1,30 @@
import { create } from "zustand";
+import type { AccountListSort, AccountListSortKey } from "@/features/dashboard/components/account-list";
+
const ACCOUNT_BURNRATE_STORAGE_KEY = "codex-lb-account-burnrate-enabled";
const ACCOUNT_VIEW_MODE_STORAGE_KEY = "codex-lb-dashboard-account-view-mode";
+const ACCOUNT_LIST_SORT_STORAGE_KEY = "codex-lb-dashboard-account-list-sort";
export type DashboardAccountViewMode = "cards" | "list";
type DashboardPreferencesState = {
accountBurnrateEnabled: boolean;
accountViewMode: DashboardAccountViewMode;
+ accountListSort: AccountListSort;
initialized: boolean;
initializePreferences: () => void;
setAccountBurnrateEnabled: (enabled: boolean) => void;
setAccountViewMode: (mode: DashboardAccountViewMode) => void;
+ setAccountListSort: (sort: AccountListSort) => void;
};
+const ACCOUNT_LIST_SORT_KEYS: AccountListSortKey[] = ["account", "status", "plan", "quota", "credits", "warmup"];
+
+function isAccountListSortKey(value: unknown): value is AccountListSortKey {
+ return typeof value === "string" && ACCOUNT_LIST_SORT_KEYS.includes(value as AccountListSortKey);
+}
+
function readStoredAccountBurnrateEnabled(): boolean | null {
if (typeof window === "undefined") {
return null;
@@ -36,6 +47,28 @@ function readStoredAccountViewMode(): DashboardAccountViewMode | null {
return stored === "cards" || stored === "list" ? stored : null;
}
+function readStoredAccountListSort(): AccountListSort {
+ if (typeof window === "undefined") {
+ return null;
+ }
+ const stored = window.localStorage.getItem(ACCOUNT_LIST_SORT_STORAGE_KEY);
+ if (!stored) {
+ return null;
+ }
+ try {
+ const parsed = JSON.parse(stored) as { key?: unknown; direction?: unknown };
+ if (
+ isAccountListSortKey(parsed.key) &&
+ (parsed.direction === "asc" || parsed.direction === "desc")
+ ) {
+ return { key: parsed.key, direction: parsed.direction };
+ }
+ } catch {
+ return null;
+ }
+ return null;
+}
+
function persistAccountBurnrateEnabled(enabled: boolean): void {
if (typeof window === "undefined") {
return;
@@ -50,16 +83,30 @@ function persistAccountViewMode(mode: DashboardAccountViewMode): void {
window.localStorage.setItem(ACCOUNT_VIEW_MODE_STORAGE_KEY, mode);
}
+function persistAccountListSort(sort: AccountListSort): void {
+ if (typeof window === "undefined") {
+ return;
+ }
+ if (sort === null) {
+ window.localStorage.removeItem(ACCOUNT_LIST_SORT_STORAGE_KEY);
+ return;
+ }
+ window.localStorage.setItem(ACCOUNT_LIST_SORT_STORAGE_KEY, JSON.stringify(sort));
+}
+
export const useDashboardPreferencesStore = create((set) => ({
accountBurnrateEnabled: true,
accountViewMode: "cards",
+ accountListSort: null,
initialized: false,
initializePreferences: () => {
const accountBurnrateEnabled = readStoredAccountBurnrateEnabled() ?? true;
const accountViewMode = readStoredAccountViewMode() ?? "cards";
+ const accountListSort = readStoredAccountListSort();
persistAccountBurnrateEnabled(accountBurnrateEnabled);
persistAccountViewMode(accountViewMode);
- set({ accountBurnrateEnabled, accountViewMode, initialized: true });
+ persistAccountListSort(accountListSort);
+ set({ accountBurnrateEnabled, accountViewMode, accountListSort, initialized: true });
},
setAccountBurnrateEnabled: (enabled) => {
persistAccountBurnrateEnabled(enabled);
@@ -69,4 +116,8 @@ export const useDashboardPreferencesStore = create((s
persistAccountViewMode(mode);
set({ accountViewMode: mode, initialized: true });
},
+ setAccountListSort: (sort) => {
+ persistAccountListSort(sort);
+ set({ accountListSort: sort, initialized: true });
+ },
}));