feat(ui): add error tooltip and status icon support to tree-view

This commit is contained in:
alejandrobailo
2026-02-23 15:23:53 +01:00
parent 0dd5401d95
commit a248ac6788
5 changed files with 84 additions and 18 deletions
+33 -5
View File
@@ -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 ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<TreeStatusIcon status={item.status} />
</span>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">{item.errorMessage}</p>
</TooltipContent>
</Tooltip>
) : (
<TreeStatusIcon status={item.status} />
)
) : 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 && <TreeSpinner />}
{!item.isLoading && item.status && (
<TreeStatusIcon status={item.status} />
{!showCheckboxes && item.isLoading && <TreeSpinner />}
{!showCheckboxes && statusIcon}
{showCheckboxes && shouldReplaceCheckboxWithState && (
<>
{item.isLoading && <TreeSpinner />}
{statusIcon}
</>
)}
{showCheckboxes && (
{showCheckboxes && !shouldReplaceCheckboxWithState && (
<Checkbox
size="sm"
checked={isSelected}
+25 -8
View File
@@ -5,6 +5,11 @@ import { ChevronRightIcon } from "lucide-react";
import { KeyboardEvent } from "react";
import { Checkbox } from "@/components/shadcn/checkbox";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/shadcn/tooltip";
import { cn } from "@/lib/utils";
import { TreeNodeProps } from "@/types/tree";
@@ -34,11 +39,27 @@ export function TreeNode({
onExpandedChange,
showCheckboxes,
renderItem,
expandAll,
enableSelectChildren,
}: TreeNodeProps) {
const isExpanded = expandAll || expandedIds.includes(item.id);
const isExpanded = expandedIds.includes(item.id);
const isSelected = selectedIds.includes(item.id);
const statusIcon =
!item.isLoading && item.status ? (
item.status === "error" && item.errorMessage ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<TreeStatusIcon status={item.status} />
</span>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">{item.errorMessage}</p>
</TooltipContent>
</Tooltip>
) : (
<TreeStatusIcon status={item.status} />
)
) : 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({
)}
</button>
{!item.isLoading && item.status && (
<TreeStatusIcon status={item.status} />
)}
{statusIcon}
{showCheckboxes && (
<Checkbox
@@ -170,7 +188,7 @@ export function TreeNode({
>
{item.children?.map((child) => (
<li key={child.id}>
{child.children ? (
{child.children && child.children.length > 0 ? (
<TreeNode
item={child}
level={level + 1}
@@ -180,7 +198,6 @@ export function TreeNode({
onExpandedChange={onExpandedChange}
showCheckboxes={showCheckboxes}
renderItem={renderItem}
expandAll={expandAll}
enableSelectChildren={enableSelectChildren}
/>
) : (
+23 -3
View File
@@ -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<string[]>([]);
const [internalExpandedIds, setInternalExpandedIds] = useState<string[]>([]);
const [internalExpandedIds, setInternalExpandedIds] = useState<string[]>(
expandAll ? getInitialExpandedIds(data) : [],
);
const selectedIds = controlledSelectedIds ?? internalSelectedIds;
const expandedIds = controlledExpandedIds ?? internalExpandedIds;
@@ -108,7 +129,7 @@ export function TreeView({
<ul className="space-y-1">
{items.map((item) => (
<li key={item.id}>
{item.children ? (
{item.children && item.children.length > 0 ? (
<TreeNode
item={item}
level={0}
@@ -118,7 +139,6 @@ export function TreeView({
onExpandedChange={handleExpandedChange}
showCheckboxes={showCheckboxes}
renderItem={renderItem}
expandAll={expandAll}
enableSelectChildren={enableSelectChildren}
/>
) : (
+1 -1
View File
@@ -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.
+2 -1
View File
@@ -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;
}