From a248ac67888b9c884fbff3e3e0da61e920acccc8 Mon Sep 17 00:00:00 2001 From: alejandrobailo Date: Mon, 23 Feb 2026 15:23:53 +0100 Subject: [PATCH] feat(ui): add error tooltip and status icon support to tree-view --- ui/components/shadcn/tree-view/tree-leaf.tsx | 38 +++++++++++++++++--- ui/components/shadcn/tree-view/tree-node.tsx | 33 ++++++++++++----- ui/components/shadcn/tree-view/tree-view.tsx | 26 ++++++++++++-- ui/components/shadcn/tree-view/utils.ts | 2 +- ui/types/tree.ts | 3 +- 5 files changed, 84 insertions(+), 18 deletions(-) diff --git a/ui/components/shadcn/tree-view/tree-leaf.tsx b/ui/components/shadcn/tree-view/tree-leaf.tsx index a9cb6c815b..ad95c0a002 100644 --- a/ui/components/shadcn/tree-view/tree-leaf.tsx +++ b/ui/components/shadcn/tree-view/tree-leaf.tsx @@ -3,6 +3,11 @@ import { KeyboardEvent } from "react"; import { Checkbox } from "@/components/shadcn/checkbox"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/shadcn/tooltip"; import { cn } from "@/lib/utils"; import { TreeLeafProps } from "@/types/tree"; @@ -30,6 +35,25 @@ export function TreeLeaf({ renderItem, }: TreeLeafProps) { const isSelected = selectedIds.includes(item.id); + const shouldReplaceCheckboxWithState = + showCheckboxes && (item.isLoading || Boolean(item.status)); + const statusIcon = + !item.isLoading && item.status ? ( + item.status === "error" && item.errorMessage ? ( + + + + + + + +

{item.errorMessage}

+
+
+ ) : ( + + ) + ) : null; const handleSelect = () => { if (!item.disabled) { @@ -50,7 +74,6 @@ export function TreeLeaf({ "flex items-center gap-2 rounded-md px-2 py-1.5", "hover:bg-prowler-white/5 cursor-pointer", "focus-visible:ring-border-input-primary-press focus-visible:ring-2 focus-visible:outline-none", - isSelected && "bg-prowler-white/10", item.disabled && "cursor-not-allowed opacity-50", item.className, )} @@ -62,12 +85,17 @@ export function TreeLeaf({ aria-selected={isSelected} aria-disabled={item.disabled} > - {item.isLoading && } - {!item.isLoading && item.status && ( - + {!showCheckboxes && item.isLoading && } + {!showCheckboxes && statusIcon} + + {showCheckboxes && shouldReplaceCheckboxWithState && ( + <> + {item.isLoading && } + {statusIcon} + )} - {showCheckboxes && ( + {showCheckboxes && !shouldReplaceCheckboxWithState && ( + + + + + + +

{item.errorMessage}

+
+ + ) : ( + + ) + ) : null; // Calculate indeterminate state based on descendant selection const descendantIds = getAllDescendantIds(item); @@ -91,7 +112,6 @@ export function TreeNode({ "flex items-center gap-2 rounded-md px-2 py-1.5", "hover:bg-prowler-white/5 cursor-pointer", "focus-visible:ring-border-input-primary-press focus-visible:ring-2 focus-visible:outline-none", - isSelected && "bg-prowler-white/10", item.disabled && "cursor-not-allowed opacity-50", item.className, )} @@ -125,9 +145,7 @@ export function TreeNode({ )} - {!item.isLoading && item.status && ( - - )} + {statusIcon} {showCheckboxes && ( {item.children?.map((child) => (
  • - {child.children ? ( + {child.children && child.children.length > 0 ? ( ) : ( diff --git a/ui/components/shadcn/tree-view/tree-view.tsx b/ui/components/shadcn/tree-view/tree-view.tsx index 940574677d..e749f033c2 100644 --- a/ui/components/shadcn/tree-view/tree-view.tsx +++ b/ui/components/shadcn/tree-view/tree-view.tsx @@ -9,6 +9,25 @@ import { TreeLeaf } from "./tree-leaf"; import { TreeNode } from "./tree-node"; import { getAllDescendantIds } from "./utils"; +function getInitialExpandedIds(data: TreeDataItem[] | TreeDataItem): string[] { + const items = Array.isArray(data) ? data : [data]; + + const expandableIds: string[] = []; + const stack = [...items]; + + while (stack.length > 0) { + const current = stack.pop(); + if (!current) continue; + + if (current.children && current.children.length > 0) { + expandableIds.push(current.id); + stack.push(...current.children); + } + } + + return expandableIds; +} + /** * TreeView component for rendering hierarchical data structures. * @@ -56,7 +75,9 @@ export function TreeView({ renderItem, }: TreeViewProps) { const [internalSelectedIds, setInternalSelectedIds] = useState([]); - const [internalExpandedIds, setInternalExpandedIds] = useState([]); + const [internalExpandedIds, setInternalExpandedIds] = useState( + expandAll ? getInitialExpandedIds(data) : [], + ); const selectedIds = controlledSelectedIds ?? internalSelectedIds; const expandedIds = controlledExpandedIds ?? internalExpandedIds; @@ -108,7 +129,7 @@ export function TreeView({
      {items.map((item) => (
    • - {item.children ? ( + {item.children && item.children.length > 0 ? ( ) : ( diff --git a/ui/components/shadcn/tree-view/utils.ts b/ui/components/shadcn/tree-view/utils.ts index 1df696518d..f1969fd72d 100644 --- a/ui/components/shadcn/tree-view/utils.ts +++ b/ui/components/shadcn/tree-view/utils.ts @@ -5,7 +5,7 @@ import { TreeDataItem } from "@/types/tree"; * Used to calculate consistent padding for nested tree items. */ export const TREE_INDENT_REM = 1.25; -export const TREE_LEAF_EXTRA_PADDING_REM = 1.5; +export const TREE_LEAF_EXTRA_PADDING_REM = 1.75; /** * Calculates the left padding for a tree node based on its nesting level. diff --git a/ui/types/tree.ts b/ui/types/tree.ts index ff18bf77c5..d306f01238 100644 --- a/ui/types/tree.ts +++ b/ui/types/tree.ts @@ -35,6 +35,8 @@ export interface TreeDataItem { isLoading?: boolean; /** Status indicator shown after loading (success/error) */ status?: TreeItemStatus; + /** Optional error detail used by status icon tooltip */ + errorMessage?: string; /** Additional CSS classes for the item */ className?: string; } @@ -97,7 +99,6 @@ export interface TreeNodeProps { onExpandedChange: (ids: string[]) => void; showCheckboxes: boolean; renderItem?: (params: TreeRenderItemParams) => React.ReactNode; - expandAll: boolean; enableSelectChildren: boolean; }