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() {