mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-08-14 19:25:54 +08:00
### What problem does this PR solve? Feat: Add TreeView component #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
parent
09291db805
commit
57cbefa589
120
web/package-lock.json
generated
120
web/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"@js-preview/excel": "^1.7.8",
|
"@js-preview/excel": "^1.7.8",
|
||||||
"@lexical/react": "^0.23.1",
|
"@lexical/react": "^0.23.1",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.3",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
@ -4563,6 +4564,125 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||||
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-accordion/-/react-accordion-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-collapsible": "1.1.3",
|
||||||
|
"@radix-ui/react-collection": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-alert-dialog": {
|
"node_modules/@radix-ui/react-alert-dialog": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.4.tgz",
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.4.tgz",
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
"@js-preview/excel": "^1.7.8",
|
"@js-preview/excel": "^1.7.8",
|
||||||
"@lexical/react": "^0.23.1",
|
"@lexical/react": "^0.23.1",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.3",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
|
58
web/src/components/ui/accordion.tsx
Normal file
58
web/src/components/ui/accordion.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root;
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn('border-b', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AccordionItem.displayName = 'AccordionItem';
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
));
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
));
|
||||||
|
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
358
web/src/components/ui/tree-view.tsx
Normal file
358
web/src/components/ui/tree-view.tsx
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||||
|
import { cva } from 'class-variance-authority';
|
||||||
|
import { ChevronRight } from 'lucide-react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const treeVariants = cva(
|
||||||
|
'group hover:before:opacity-100 before:absolute before:rounded-lg before:left-0 px-2 before:w-full before:opacity-0 before:bg-accent/70 before:h-[2rem] before:-z-10',
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedTreeVariants = cva(
|
||||||
|
'before:opacity-100 before:bg-accent/70 text-accent-foreground',
|
||||||
|
);
|
||||||
|
|
||||||
|
interface TreeDataItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon?: any;
|
||||||
|
selectedIcon?: any;
|
||||||
|
openIcon?: any;
|
||||||
|
children?: TreeDataItem[];
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header>
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-1 w-full items-center py-2 transition-all first:[&[data-state=open]>svg]:rotate-90',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4 shrink-0 transition-transform duration-200 text-accent-foreground/50 mr-1" />
|
||||||
|
{children}
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
));
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="pb-1 pt-0">{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
));
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const TreeIcon = ({
|
||||||
|
item,
|
||||||
|
isOpen,
|
||||||
|
isSelected,
|
||||||
|
default: defaultIcon,
|
||||||
|
}: {
|
||||||
|
item: TreeDataItem;
|
||||||
|
isOpen?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
default?: any;
|
||||||
|
}) => {
|
||||||
|
let Icon = defaultIcon;
|
||||||
|
if (isSelected && item.selectedIcon) {
|
||||||
|
Icon = item.selectedIcon;
|
||||||
|
} else if (isOpen && item.openIcon) {
|
||||||
|
Icon = item.openIcon;
|
||||||
|
} else if (item.icon) {
|
||||||
|
Icon = item.icon;
|
||||||
|
}
|
||||||
|
return Icon ? <Icon className="h-4 w-4 shrink-0 mr-2" /> : <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TreeActions = ({
|
||||||
|
children,
|
||||||
|
isSelected,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
isSelected: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
isSelected ? 'block' : 'hidden',
|
||||||
|
'absolute right-3 group-hover:block',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TreeNode = ({
|
||||||
|
item,
|
||||||
|
handleSelectChange,
|
||||||
|
expandedItemIds,
|
||||||
|
selectedItemId,
|
||||||
|
defaultNodeIcon,
|
||||||
|
defaultLeafIcon,
|
||||||
|
}: {
|
||||||
|
item: TreeDataItem;
|
||||||
|
handleSelectChange: (item: TreeDataItem | undefined) => void;
|
||||||
|
expandedItemIds: string[];
|
||||||
|
selectedItemId?: string;
|
||||||
|
defaultNodeIcon?: any;
|
||||||
|
defaultLeafIcon?: any;
|
||||||
|
}) => {
|
||||||
|
const [value, setValue] = React.useState(
|
||||||
|
expandedItemIds.includes(item.id) ? [item.id] : [],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Root
|
||||||
|
type="multiple"
|
||||||
|
value={value}
|
||||||
|
onValueChange={(s) => setValue(s)}
|
||||||
|
>
|
||||||
|
<AccordionPrimitive.Item value={item.id}>
|
||||||
|
<AccordionTrigger
|
||||||
|
className={cn(
|
||||||
|
treeVariants(),
|
||||||
|
selectedItemId === item.id && selectedTreeVariants(),
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
handleSelectChange(item);
|
||||||
|
item.onClick?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TreeIcon
|
||||||
|
item={item}
|
||||||
|
isSelected={selectedItemId === item.id}
|
||||||
|
isOpen={value.includes(item.id)}
|
||||||
|
default={defaultNodeIcon}
|
||||||
|
/>
|
||||||
|
<span className="text-sm truncate">{item.name}</span>
|
||||||
|
<TreeActions isSelected={selectedItemId === item.id}>
|
||||||
|
{item.actions}
|
||||||
|
</TreeActions>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="ml-4 pl-1 border-l">
|
||||||
|
<TreeItem
|
||||||
|
data={item.children ? item.children : item}
|
||||||
|
selectedItemId={selectedItemId}
|
||||||
|
handleSelectChange={handleSelectChange}
|
||||||
|
expandedItemIds={expandedItemIds}
|
||||||
|
defaultLeafIcon={defaultLeafIcon}
|
||||||
|
defaultNodeIcon={defaultNodeIcon}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionPrimitive.Item>
|
||||||
|
</AccordionPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type TreeItemProps = TreeProps & {
|
||||||
|
selectedItemId?: string;
|
||||||
|
handleSelectChange: (item: TreeDataItem | undefined) => void;
|
||||||
|
expandedItemIds: string[];
|
||||||
|
defaultNodeIcon?: any;
|
||||||
|
defaultLeafIcon?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
data,
|
||||||
|
selectedItemId,
|
||||||
|
handleSelectChange,
|
||||||
|
expandedItemIds,
|
||||||
|
defaultNodeIcon,
|
||||||
|
defaultLeafIcon,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
if (!(data instanceof Array)) {
|
||||||
|
data = [data];
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div ref={ref} role="tree" className={className} {...props}>
|
||||||
|
<ul>
|
||||||
|
{data.map((item) => (
|
||||||
|
<li key={item.id}>
|
||||||
|
{item.children ? (
|
||||||
|
<TreeNode
|
||||||
|
item={item}
|
||||||
|
selectedItemId={selectedItemId}
|
||||||
|
expandedItemIds={expandedItemIds}
|
||||||
|
handleSelectChange={handleSelectChange}
|
||||||
|
defaultNodeIcon={defaultNodeIcon}
|
||||||
|
defaultLeafIcon={defaultLeafIcon}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TreeLeaf
|
||||||
|
item={item}
|
||||||
|
selectedItemId={selectedItemId}
|
||||||
|
handleSelectChange={handleSelectChange}
|
||||||
|
defaultLeafIcon={defaultLeafIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
TreeItem.displayName = 'TreeItem';
|
||||||
|
|
||||||
|
const TreeLeaf = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
item: TreeDataItem;
|
||||||
|
selectedItemId?: string;
|
||||||
|
handleSelectChange: (item: TreeDataItem | undefined) => void;
|
||||||
|
defaultLeafIcon?: any;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
item,
|
||||||
|
selectedItemId,
|
||||||
|
handleSelectChange,
|
||||||
|
defaultLeafIcon,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'ml-5 flex text-left items-center py-2 cursor-pointer before:right-1',
|
||||||
|
treeVariants(),
|
||||||
|
className,
|
||||||
|
selectedItemId === item.id && selectedTreeVariants(),
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
handleSelectChange(item);
|
||||||
|
item.onClick?.();
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<TreeIcon
|
||||||
|
item={item}
|
||||||
|
isSelected={selectedItemId === item.id}
|
||||||
|
default={defaultLeafIcon}
|
||||||
|
/>
|
||||||
|
<span className="flex-grow text-sm truncate">{item.name}</span>
|
||||||
|
<TreeActions isSelected={selectedItemId === item.id}>
|
||||||
|
{item.actions}
|
||||||
|
</TreeActions>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
TreeLeaf.displayName = 'TreeLeaf';
|
||||||
|
|
||||||
|
type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
data: TreeDataItem[] | TreeDataItem;
|
||||||
|
initialSelectedItemId?: string;
|
||||||
|
onSelectChange?: (item: TreeDataItem | undefined) => void;
|
||||||
|
expandAll?: boolean;
|
||||||
|
defaultNodeIcon?: any;
|
||||||
|
defaultLeafIcon?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
initialSelectedItemId,
|
||||||
|
onSelectChange,
|
||||||
|
expandAll,
|
||||||
|
defaultLeafIcon,
|
||||||
|
defaultNodeIcon,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const [selectedItemId, setSelectedItemId] = React.useState<
|
||||||
|
string | undefined
|
||||||
|
>(initialSelectedItemId);
|
||||||
|
|
||||||
|
const handleSelectChange = React.useCallback(
|
||||||
|
(item: TreeDataItem | undefined) => {
|
||||||
|
setSelectedItemId(item?.id);
|
||||||
|
if (onSelectChange) {
|
||||||
|
onSelectChange(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSelectChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const expandedItemIds = React.useMemo(() => {
|
||||||
|
if (!initialSelectedItemId) {
|
||||||
|
return [] as string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids: string[] = [];
|
||||||
|
|
||||||
|
function walkTreeItems(
|
||||||
|
items: TreeDataItem[] | TreeDataItem,
|
||||||
|
targetId: string,
|
||||||
|
) {
|
||||||
|
if (items instanceof Array) {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
ids.push(items[i]!.id);
|
||||||
|
if (walkTreeItems(items[i]!, targetId) && !expandAll) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!expandAll) ids.pop();
|
||||||
|
}
|
||||||
|
} else if (!expandAll && items.id === targetId) {
|
||||||
|
return true;
|
||||||
|
} else if (items.children) {
|
||||||
|
return walkTreeItems(items.children, targetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walkTreeItems(data, initialSelectedItemId);
|
||||||
|
return ids;
|
||||||
|
}, [data, expandAll, initialSelectedItemId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('overflow-hidden relative p-2', className)}>
|
||||||
|
<TreeItem
|
||||||
|
data={data}
|
||||||
|
ref={ref}
|
||||||
|
selectedItemId={selectedItemId}
|
||||||
|
handleSelectChange={handleSelectChange}
|
||||||
|
expandedItemIds={expandedItemIds}
|
||||||
|
defaultLeafIcon={defaultLeafIcon}
|
||||||
|
defaultNodeIcon={defaultNodeIcon}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
TreeView.displayName = 'TreeView';
|
||||||
|
|
||||||
|
export { TreeView, type TreeDataItem };
|
Loading…
x
Reference in New Issue
Block a user