mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-01 00:05:43 +08:00
Fix: The folder tree menu for moving folders cannot be scrolled. (#15037)
### What problem does this PR solve? Fix: The folder tree menu for moving folders cannot be scrolled. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
@@ -9,6 +9,7 @@ import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from './button';
|
||||
import { SearchInput } from './input';
|
||||
|
||||
type TreeId = number | string;
|
||||
|
||||
@@ -26,6 +27,13 @@ type AsyncTreeSelectProps = {
|
||||
loadData?(node: TreeNodeType): Promise<any>;
|
||||
};
|
||||
|
||||
function getNodeText(node: ReactNode): string {
|
||||
if (typeof node === 'string') return node;
|
||||
if (typeof node === 'number') return String(node);
|
||||
if (Array.isArray(node)) return node.map(getNodeText).join('');
|
||||
return '';
|
||||
}
|
||||
|
||||
export function AsyncTreeSelect({
|
||||
treeData,
|
||||
value,
|
||||
@@ -33,6 +41,7 @@ export function AsyncTreeSelect({
|
||||
onChange,
|
||||
}: AsyncTreeSelectProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [expandedKeys, setExpandedKeys] = useState<TreeId[]>([]);
|
||||
@@ -42,14 +51,59 @@ export function AsyncTreeSelect({
|
||||
return treeData.find((x) => x.id === value)?.title;
|
||||
}, [treeData, value]);
|
||||
|
||||
const visibleNodeIds = useMemo(() => {
|
||||
const trimmed = searchText.trim().toLowerCase();
|
||||
if (!trimmed) {
|
||||
return new Set(treeData.map((n) => n.id));
|
||||
}
|
||||
|
||||
const matchedIds = new Set<TreeId>();
|
||||
treeData.forEach((node) => {
|
||||
if (getNodeText(node.title).toLowerCase().includes(trimmed)) {
|
||||
matchedIds.add(node.id);
|
||||
}
|
||||
});
|
||||
|
||||
const visibleIds = new Set<TreeId>(matchedIds);
|
||||
|
||||
const addAncestors = (nodeId: TreeId) => {
|
||||
const node = treeData.find((n) => n.id === nodeId);
|
||||
if (node && node.parentId) {
|
||||
visibleIds.add(node.parentId);
|
||||
addAncestors(node.parentId);
|
||||
}
|
||||
};
|
||||
matchedIds.forEach((id) => addAncestors(id));
|
||||
|
||||
const queue = Array.from(matchedIds);
|
||||
let i = 0;
|
||||
while (i < queue.length) {
|
||||
const currentId = queue[i++];
|
||||
treeData.forEach((node) => {
|
||||
if (node.parentId === currentId) {
|
||||
visibleIds.add(node.id);
|
||||
queue.push(node.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return visibleIds;
|
||||
}, [treeData, searchText]);
|
||||
|
||||
const isExpanded = useCallback(
|
||||
(id: TreeId | undefined) => {
|
||||
if (id === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (searchText.trim()) {
|
||||
const hasVisibleChild = treeData.some(
|
||||
(n) => n.parentId === id && visibleNodeIds.has(n.id),
|
||||
);
|
||||
if (hasVisibleChild) return true;
|
||||
}
|
||||
return expandedKeys.indexOf(id) !== -1;
|
||||
},
|
||||
[expandedKeys],
|
||||
[expandedKeys, searchText, treeData, visibleNodeIds],
|
||||
);
|
||||
|
||||
const handleNodeClick = useCallback(
|
||||
@@ -57,6 +111,7 @@ export function AsyncTreeSelect({
|
||||
e.stopPropagation();
|
||||
onChange?.(id);
|
||||
setOpen(false);
|
||||
setSearchText('');
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
@@ -85,11 +140,24 @@ export function AsyncTreeSelect({
|
||||
[isExpanded, loadData, treeData],
|
||||
);
|
||||
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open);
|
||||
if (!open) {
|
||||
setSearchText('');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderNodes = useCallback(
|
||||
(parentId?: TreeId) => {
|
||||
const currentLevelList = parentId
|
||||
? treeData.filter((x) => x.parentId === parentId)
|
||||
: treeData.filter((x) => treeData.every((y) => x.parentId !== y.id));
|
||||
? treeData.filter(
|
||||
(x) => x.parentId === parentId && visibleNodeIds.has(x.id),
|
||||
)
|
||||
: treeData.filter(
|
||||
(x) =>
|
||||
treeData.every((y) => x.parentId !== y.id) &&
|
||||
visibleNodeIds.has(x.id),
|
||||
);
|
||||
|
||||
if (currentLevelList.length === 0) return null;
|
||||
|
||||
@@ -99,15 +167,15 @@ export function AsyncTreeSelect({
|
||||
<li
|
||||
key={x.id}
|
||||
onClick={handleNodeClick(x.id)}
|
||||
className="cursor-pointer "
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-between items-center hover:bg-accent py-0.5 px-1 rounded-md ',
|
||||
'flex justify-between items-center hover:bg-accent py-0.5 px-1 rounded-md',
|
||||
{ 'bg-cyan-50': value === x.id },
|
||||
)}
|
||||
>
|
||||
<span className={cn('flex-1 ')}>{x.title}</span>
|
||||
<span className="flex-1">{x.title}</span>
|
||||
{x.isLeaf || (
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
@@ -131,27 +199,48 @@ export function AsyncTreeSelect({
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
[handleArrowClick, handleNodeClick, isExpanded, loadingId, treeData, value],
|
||||
[
|
||||
handleArrowClick,
|
||||
handleNodeClick,
|
||||
isExpanded,
|
||||
loadingId,
|
||||
treeData,
|
||||
value,
|
||||
visibleNodeIds,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(treeData)) {
|
||||
loadData?.({ id: '', parentId: '', title: '' });
|
||||
}
|
||||
}, [loadData, treeData]);
|
||||
}, [treeData, loadData]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="flex justify-between border px-2 py-1.5 rounded-md gap-2 items-center w-full">
|
||||
{selectedTitle || (
|
||||
<span className="text-slate-400">{t('common.pleaseSelect')}</span>
|
||||
)}
|
||||
<ChevronDown className="size-5 " />
|
||||
<ChevronDown className="size-5" />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-1 min-w-[var(--radix-popover-trigger-width)]">
|
||||
<ul>{renderNodes()}</ul>
|
||||
<SearchInput
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
></SearchInput>
|
||||
<ul
|
||||
className="max-h-[40vh] overflow-auto"
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
{renderNodes() || (
|
||||
<div className="px-2 py-4 text-center text-sm text-text-disabled">
|
||||
{t('common.noDataFound')}
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user