From 9a369b5520fa8dc531eecfa6ee34bd7fd89132d0 Mon Sep 17 00:00:00 2001 From: Chris Sherwood Date: Mon, 18 May 2026 15:23:18 -0700 Subject: [PATCH] fix(KB): TierSelectionModal hook order + register IconLibrary Two related fixes surfaced by armandoescalante in #915 when clicking a Content Explorer category card (e.g. Medicine) on v1.32.0-rc.6: 1. TierSelectionModal placed a useMemo for freeBytes *after* the `if (!category) return null` early return (introduced in PR #901's guardrail integration). When `category` transitioned from null to non-null on first open, React saw a different hook count between renders and crashed the entire component tree with "Rendered more hooks than during the previous render", blanking the modal. Moved the freeBytes useMemo above the early return so hook order is constant. 2. `IconLibrary` was used as the icon prop on the Manage Custom Libraries button in remote-explorer.tsx but never registered in the DynamicIcon allowlist at admin/inertia/lib/icons.ts. Added it to both the import block and the icons map so the warning stops firing and the icon renders. Closes #915. --- .../inertia/components/TierSelectionModal.tsx | 20 +++++++++++-------- admin/inertia/lib/icons.ts | 2 ++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/admin/inertia/components/TierSelectionModal.tsx b/admin/inertia/components/TierSelectionModal.tsx index 3db5094..255adb3 100644 --- a/admin/inertia/components/TierSelectionModal.tsx +++ b/admin/inertia/components/TierSelectionModal.tsx @@ -96,6 +96,18 @@ const TierSelectionModal: React.FC = ({ // underneath. Cancel returns to the tier modal as-is; Proceed closes both // and runs the original onSelectTier path. const [guardrailVerdict, setGuardrailVerdict] = useState(null) + + // Compute disk-free bytes from system info; 0 means "unknown", which the + // guardrail helper treats as "skip the relative-disk check". + // Must be declared before the `!category` early return so the hook count + // stays constant across renders (category transitions null → non-null when + // the user opens the modal). + const freeBytes = useMemo(() => { + const primary = getPrimaryDiskInfo(systemInfo?.disk, systemInfo?.fsSize) + if (!primary) return 0 + return Math.max(0, primary.totalSize - primary.totalUsed) + }, [systemInfo]) + const ingestPolicy: 'Always' | 'Manual' = ingestPolicySetting?.value === 'Manual' ? 'Manual' : 'Always' @@ -114,14 +126,6 @@ const TierSelectionModal: React.FC = ({ } } - // Compute disk-free bytes from system info; 0 means "unknown", which the - // guardrail helper treats as "skip the relative-disk check". - const freeBytes = useMemo(() => { - const primary = getPrimaryDiskInfo(systemInfo?.disk, systemInfo?.fsSize) - if (!primary) return 0 - return Math.max(0, primary.totalSize - primary.totalUsed) - }, [systemInfo]) - /** * Runs the original onSelectTier-then-onClose flow. Pulled out of * handleSubmit so the guardrail modal's confirm path can call it after diff --git a/admin/inertia/lib/icons.ts b/admin/inertia/lib/icons.ts index cf9484d..75a039d 100644 --- a/admin/inertia/lib/icons.ts +++ b/admin/inertia/lib/icons.ts @@ -32,6 +32,7 @@ import { IconInfoCircle, IconBug, IconCopy, + IconLibrary, IconServer, IconMenu2, IconArrowLeft, @@ -75,6 +76,7 @@ export const icons = { IconDownload, IconHome, IconInfoCircle, + IconLibrary, IconLogs, IconMap, IconMenu2,