From b882c299e7078a731a491f52da60172c68163098 Mon Sep 17 00:00:00 2001
From: LeekJay <314964866@qq.com>
Date: Tue, 9 Jun 2026 20:26:10 +0800
Subject: [PATCH 1/3] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E5=A4=8D=20asChild=20?=
=?UTF-8?q?=E6=8C=89=E9=92=AE=20Slot=20=E5=AD=90=E8=8A=82=E7=82=B9?=
=?UTF-8?q?=E5=B4=A9=E6=BA=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
删除确认弹窗中的 AlertDialogAction 通过 Button asChild 组合 Radix Slot,按钮内部额外渲染 loading 图标会让 Slot 收到多个子节点并导致 /ai-providers 删除操作崩溃,本次修复 asChild 场景的子节点结构。
变更:
- 将 loading 图标抽出为可复用节点
- 在 asChild 且 loading 时把图标注入唯一的 React 子元素
- 保持普通 button 场景继续渲染 loading 图标与原 children
影响:
- 修复 /ai-providers 点击删除时的 Radix Slot 崩溃
- 保留 Button 组件现有 loading 表现,降低对其他按钮的回归风险
---
src/components/ui/button.tsx | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 6d46597..56c2811 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -63,6 +63,18 @@ function Button({
loading?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
+ const loader = loading ? (
+
+ ) : null
+ const slottedChildren =
+ asChild && loader && React.isValidElement(children)
+ ? React.cloneElement(
+ children,
+ undefined,
+ loader,
+ (children.props as { children?: React.ReactNode }).children
+ )
+ : children
return (
- {loading && }
- {children}
+ {asChild ? (
+ slottedChildren
+ ) : (
+ <>
+ {loader}
+ {children}
+ >
+ )}
)
}
From 6e0870129d1725ae3e0630a0a2718cbd6b2f0346 Mon Sep 17 00:00:00 2001
From: LeekJay <314964866@qq.com>
Date: Tue, 9 Jun 2026 21:54:52 +0800
Subject: [PATCH 2/3] =?UTF-8?q?fix(lint):=20=E4=BF=AE=E5=A4=8D=E7=8E=B0?=
=?UTF-8?q?=E6=9C=89=E5=89=8D=E7=AB=AF=20lint=20=E9=98=BB=E5=A1=9E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
前端 lint 当前被 AuthFilesPage 同步 setState、Fast Refresh 非组件导出以及 ConfigPage 不稳定 hook 依赖阻塞,本次集中修复这些已知问题以恢复 lint 通过。
变更:
- 将 AuthFilesPage 持久化状态改为 lazy initializer,并把批量操作栏打开动作移到用户事件
- 拆出 Button、Toggle variants 与 Sidebar context,收窄 UI 组件文件导出
- 将 ConfigPage 的 handleSave 包装为 useCallback,稳定 pageActions 依赖
影响:
- bun run lint 可通过,减少 Fast Refresh 与 hooks 规则噪声
- 保留批量操作栏动画与现有按钮样式行为
---
src/components/app-sidebar.tsx | 2 +-
src/components/common/LobeProviderIcon.tsx | 8 +-
src/components/config/VisualConfigEditor.tsx | 2 +-
src/components/ui/badge.tsx | 2 +-
src/components/ui/button-group.tsx | 1 -
src/components/ui/button-variants.ts | 46 ++++++++
src/components/ui/button.tsx | 47 +-------
src/components/ui/sidebar-context.ts | 24 ++++
src/components/ui/sidebar.tsx | 27 +----
src/components/ui/tabs.tsx | 2 +-
src/components/ui/toggle-group.tsx | 9 +-
src/components/ui/toggle-variants.ts | 27 +++++
src/components/ui/toggle.tsx | 28 +----
src/pages/AuthFilesPage.tsx | 111 ++++++++++++-------
src/pages/ConfigPage.tsx | 13 ++-
15 files changed, 201 insertions(+), 148 deletions(-)
create mode 100644 src/components/ui/button-variants.ts
create mode 100644 src/components/ui/sidebar-context.ts
create mode 100644 src/components/ui/toggle-variants.ts
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx
index a0699d2..9560f27 100644
--- a/src/components/app-sidebar.tsx
+++ b/src/components/app-sidebar.tsx
@@ -32,8 +32,8 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
- useSidebar,
} from '@/components/ui/sidebar';
+import { useSidebar } from '@/components/ui/sidebar-context';
import { cn } from '@/lib/utils';
export interface SidebarNavItem {
diff --git a/src/components/common/LobeProviderIcon.tsx b/src/components/common/LobeProviderIcon.tsx
index 8b3359d..21d7117 100644
--- a/src/components/common/LobeProviderIcon.tsx
+++ b/src/components/common/LobeProviderIcon.tsx
@@ -61,7 +61,7 @@ const PROVIDER_ICON_MAP: Record = {
'x-ai': XAIIcon,
};
-export interface LobeProviderIconProps {
+interface LobeProviderIconProps {
className?: string;
fallbackLabel?: string;
provider?: string | null;
@@ -70,16 +70,12 @@ export interface LobeProviderIconProps {
title?: string;
}
-export const normalizeLobeProviderKey = (value?: string | null) =>
+const normalizeLobeProviderKey = (value?: string | null) =>
String(value ?? '')
.trim()
.toLowerCase()
.replace(/[\s_]+/g, '-');
-export function hasLobeProviderIcon(provider?: string | null): boolean {
- return Boolean(PROVIDER_ICON_MAP[normalizeLobeProviderKey(provider)]);
-}
-
export function LobeProviderIcon({
className,
fallbackLabel,
diff --git a/src/components/config/VisualConfigEditor.tsx b/src/components/config/VisualConfigEditor.tsx
index 853d177..8ec287a 100644
--- a/src/components/config/VisualConfigEditor.tsx
+++ b/src/components/config/VisualConfigEditor.tsx
@@ -18,7 +18,7 @@ import {
TimerIcon,
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
-import { buttonVariants } from '@/components/ui/button';
+import { buttonVariants } from '@/components/ui/button-variants';
import {
Field,
FieldDescription,
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
index cacff11..2eb5513 100644
--- a/src/components/ui/badge.tsx
+++ b/src/components/ui/badge.tsx
@@ -46,4 +46,4 @@ function Badge({
)
}
-export { Badge, badgeVariants }
+export { Badge }
diff --git a/src/components/ui/button-group.tsx b/src/components/ui/button-group.tsx
index 692fb07..ebab590 100644
--- a/src/components/ui/button-group.tsx
+++ b/src/components/ui/button-group.tsx
@@ -79,5 +79,4 @@ export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
- buttonGroupVariants,
}
diff --git a/src/components/ui/button-variants.ts b/src/components/ui/button-variants.ts
new file mode 100644
index 0000000..186216b
--- /dev/null
+++ b/src/components/ui/button-variants.ts
@@ -0,0 +1,46 @@
+import { cva, type VariantProps } from "class-variance-authority"
+
+const buttonVariants = cva(
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
+ primary: "bg-primary text-primary-foreground hover:bg-primary/80",
+ outline:
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
+ ghost:
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
+ destructive:
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
+ danger:
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default:
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ md: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ icon: "size-8",
+ "icon-xs":
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
+ "icon-sm":
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
+ "icon-lg": "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+type ButtonVariantProps = VariantProps
+
+export { buttonVariants, type ButtonVariantProps }
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 56c2811..ebe072d 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -1,51 +1,10 @@
import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
import { LoaderCircleIcon } from "lucide-react"
import { Slot } from "radix-ui"
+import { buttonVariants, type ButtonVariantProps } from "@/components/ui/button-variants"
import { cn } from "@/lib/utils"
-const buttonVariants = cva(
- "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
- {
- variants: {
- variant: {
- default: "bg-primary text-primary-foreground hover:bg-primary/80",
- primary: "bg-primary text-primary-foreground hover:bg-primary/80",
- outline:
- "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
- ghost:
- "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
- destructive:
- "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
- danger:
- "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default:
- "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- md: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
- sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
- lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- icon: "size-8",
- "icon-xs":
- "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
- "icon-sm":
- "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
- "icon-lg": "size-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-)
-
function Button({
children,
className,
@@ -57,7 +16,7 @@ function Button({
disabled,
...props
}: React.ComponentProps<"button"> &
- VariantProps & {
+ ButtonVariantProps & {
asChild?: boolean
fullWidth?: boolean
loading?: boolean
@@ -97,4 +56,4 @@ function Button({
)
}
-export { Button, buttonVariants }
+export { Button }
diff --git a/src/components/ui/sidebar-context.ts b/src/components/ui/sidebar-context.ts
new file mode 100644
index 0000000..825851b
--- /dev/null
+++ b/src/components/ui/sidebar-context.ts
@@ -0,0 +1,24 @@
+import * as React from "react"
+
+type SidebarContextProps = {
+ state: "expanded" | "collapsed"
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.")
+ }
+
+ return context
+}
+
+export { SidebarContext, useSidebar, type SidebarContextProps }
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
index 1389fc5..9585f70 100644
--- a/src/components/ui/sidebar.tsx
+++ b/src/components/ui/sidebar.tsx
@@ -7,6 +7,11 @@ import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
+import {
+ SidebarContext,
+ useSidebar,
+ type SidebarContextProps,
+} from "@/components/ui/sidebar-context"
import {
Sheet,
SheetContent,
@@ -29,27 +34,6 @@ const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
-type SidebarContextProps = {
- state: "expanded" | "collapsed"
- open: boolean
- setOpen: (open: boolean) => void
- openMobile: boolean
- setOpenMobile: (open: boolean) => void
- isMobile: boolean
- toggleSidebar: () => void
-}
-
-const SidebarContext = React.createContext(null)
-
-function useSidebar() {
- const context = React.useContext(SidebarContext)
- if (!context) {
- throw new Error("useSidebar must be used within a SidebarProvider.")
- }
-
- return context
-}
-
function SidebarProvider({
defaultOpen = true,
open: openProp,
@@ -696,5 +680,4 @@ export {
SidebarRail,
SidebarSeparator,
SidebarTrigger,
- useSidebar,
}
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
index 72465b2..432125c 100644
--- a/src/components/ui/tabs.tsx
+++ b/src/components/ui/tabs.tsx
@@ -85,4 +85,4 @@ function TabsContent({
)
}
-export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/src/components/ui/toggle-group.tsx b/src/components/ui/toggle-group.tsx
index f797b05..ec01c8f 100644
--- a/src/components/ui/toggle-group.tsx
+++ b/src/components/ui/toggle-group.tsx
@@ -1,14 +1,13 @@
"use client"
import * as React from "react"
-import { type VariantProps } from "class-variance-authority"
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
-import { toggleVariants } from "@/components/ui/toggle"
+import { toggleVariants, type ToggleVariantProps } from "@/components/ui/toggle-variants"
const ToggleGroupContext = React.createContext<
- VariantProps & {
+ ToggleVariantProps & {
spacing?: number
orientation?: "horizontal" | "vertical"
}
@@ -28,7 +27,7 @@ function ToggleGroup({
children,
...props
}: React.ComponentProps &
- VariantProps & {
+ ToggleVariantProps & {
spacing?: number
orientation?: "horizontal" | "vertical"
}) {
@@ -62,7 +61,7 @@ function ToggleGroupItem({
size = "default",
...props
}: React.ComponentProps &
- VariantProps) {
+ ToggleVariantProps) {
const context = React.useContext(ToggleGroupContext)
return (
diff --git a/src/components/ui/toggle-variants.ts b/src/components/ui/toggle-variants.ts
new file mode 100644
index 0000000..c4283f8
--- /dev/null
+++ b/src/components/ui/toggle-variants.ts
@@ -0,0 +1,27 @@
+import { cva, type VariantProps } from "class-variance-authority"
+
+const toggleVariants = cva(
+ "group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ outline: "border border-input bg-transparent hover:bg-muted",
+ },
+ size: {
+ default:
+ "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
+ lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+type ToggleVariantProps = VariantProps
+
+export { toggleVariants, type ToggleVariantProps }
diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx
index 8791a0a..1db5316 100644
--- a/src/components/ui/toggle.tsx
+++ b/src/components/ui/toggle.tsx
@@ -1,30 +1,8 @@
import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
import { Toggle as TogglePrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
-
-const toggleVariants = cva(
- "group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
- {
- variants: {
- variant: {
- default: "bg-transparent",
- outline: "border border-input bg-transparent hover:bg-muted",
- },
- size: {
- default:
- "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
- lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-)
+import { toggleVariants, type ToggleVariantProps } from "@/components/ui/toggle-variants"
function Toggle({
className,
@@ -32,7 +10,7 @@ function Toggle({
size = "default",
...props
}: React.ComponentProps &
- VariantProps) {
+ ToggleVariantProps) {
return (
('all');
- const [page, setPage] = useState(1);
+ const [initialUiState] = useState(() => readAuthFilesUiState());
+ const [filter, setFilter] = useState<'all' | string>(() => {
+ const persistedFilter = initialUiState?.filter;
+ return typeof persistedFilter === 'string' && persistedFilter.trim()
+ ? normalizeProviderKey(persistedFilter)
+ : 'all';
+ });
+ const [page, setPage] = useState(() => {
+ const persistedPage = initialUiState?.page;
+ return typeof persistedPage === 'number' && Number.isFinite(persistedPage)
+ ? Math.max(1, Math.round(persistedPage))
+ : 1;
+ });
const [viewMode, setViewMode] = useState<'diagram' | 'list'>('list');
const [accountDialogOpen, setAccountDialogOpen] = useState(false);
const [accountChannel, setAccountChannel] = useState('codex');
const [batchActionBarVisible, setBatchActionBarVisible] = useState(false);
- const [uiStateHydrated, setUiStateHydrated] = useState(false);
const floatingBatchActionsRef = useRef(null);
const batchActionAnimationRef = useRef(null);
const previousSelectionCountRef = useRef(0);
@@ -199,33 +209,12 @@ export function AuthFilesPage() {
const pageSize = DEFAULT_REGULAR_PAGE_SIZE;
useEffect(() => {
- const persisted = readAuthFilesUiState();
- if (persisted) {
- if (typeof persisted.filter === 'string' && persisted.filter.trim()) {
- setFilter(normalizeProviderKey(persisted.filter));
- }
- if (typeof persisted.page === 'number' && Number.isFinite(persisted.page)) {
- setPage(Math.max(1, Math.round(persisted.page)));
- }
- }
-
- setUiStateHydrated(true);
- }, []);
-
- useEffect(() => {
- if (!uiStateHydrated) return;
-
writeAuthFilesUiState({
filter,
page,
pageSize,
});
- }, [
- filter,
- page,
- pageSize,
- uiStateHydrated,
- ]);
+ }, [filter, page, pageSize]);
const handleHeaderRefresh = useCallback(async () => {
await Promise.all([loadFiles(), loadExcluded(), loadModelAlias()]);
@@ -346,6 +335,59 @@ export function AuthFilesPage() {
batchStatusUpdating ||
selectedHasStatusUpdating;
+ const showBatchActionBar = useCallback(() => {
+ setBatchActionBarVisible(true);
+ }, []);
+
+ const handleToggleAllPageItems = useCallback(() => {
+ if (allPageItemsSelected) {
+ invertVisibleSelection(selectablePageItems);
+ return;
+ }
+ showBatchActionBar();
+ selectAllVisible(pageItems);
+ }, [
+ allPageItemsSelected,
+ invertVisibleSelection,
+ pageItems,
+ selectablePageItems,
+ selectAllVisible,
+ showBatchActionBar,
+ ]);
+
+ const handleToggleFileSelection = useCallback(
+ (name: string) => {
+ if (!selectedFiles.has(name)) {
+ showBatchActionBar();
+ }
+ toggleSelect(name);
+ },
+ [selectedFiles, showBatchActionBar, toggleSelect]
+ );
+
+ const handleSelectPageItems = useCallback(() => {
+ showBatchActionBar();
+ selectAllVisible(pageItems);
+ }, [pageItems, selectAllVisible, showBatchActionBar]);
+
+ const handleSelectFilteredItems = useCallback(() => {
+ showBatchActionBar();
+ selectAllVisible(sorted);
+ }, [selectAllVisible, showBatchActionBar, sorted]);
+
+ const handleInvertPageSelection = useCallback(() => {
+ if (selectablePageItems.some((file) => !selectedFiles.has(file.name))) {
+ showBatchActionBar();
+ }
+ invertVisibleSelection(pageItems);
+ }, [
+ invertVisibleSelection,
+ pageItems,
+ selectablePageItems,
+ selectedFiles,
+ showBatchActionBar,
+ ]);
+
const copyTextWithNotification = useCallback(
async (text: string) => {
const copied = await copyToClipboard(text);
@@ -418,9 +460,6 @@ export function AuthFilesPage() {
useEffect(() => {
selectionCountRef.current = selectionCount;
- if (selectionCount > 0) {
- setBatchActionBarVisible(true);
- }
}, [selectionCount]);
useLayoutEffect(() => {
@@ -520,13 +559,7 @@ export function AuthFilesPage() {
? t('auth_files.batch_deselect')
: t('auth_files.batch_select_page')
}
- onCheckedChange={() => {
- if (allPageItemsSelected) {
- invertVisibleSelection(selectablePageItems);
- return;
- }
- selectAllVisible(pageItems);
- }}
+ onCheckedChange={handleToggleAllPageItems}
/>
@@ -563,7 +596,7 @@ export function AuthFilesPage() {
? t('auth_files.batch_deselect')
: t('auth_files.batch_select_all')
}
- onCheckedChange={() => toggleSelect(file.name)}
+ onCheckedChange={() => handleToggleFileSelection(file.name)}
/>
)}
@@ -925,7 +958,7 @@ export function AuthFilesPage() {