/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CSSProperties, memo } from 'react';
import type { ListChildComponentProps } from 'react-window';
import { useDroppable } from '@dnd-kit/core';
import type { UniqueIdentifier } from '@dnd-kit/core';
import type { Metric, ColumnMeta } from '@superset-ui/chart-controls';
import { FoldersEditorItemType } from '../types';
import type { FlattenedTreeItem } from './constants';
import { isDefaultFolder } from './constants';
import { TreeItem } from './TreeItem';
// Invisible placeholder that keeps the droppable area for horizontal drag depth changes
interface DragPlaceholderProps {
id: string;
style: CSSProperties;
type: FoldersEditorItemType;
isFolder: boolean;
}
const DragPlaceholder = memo(function DragPlaceholder({
id,
style,
type,
isFolder,
}: DragPlaceholderProps) {
const { setNodeRef } = useDroppable({
id,
data: { type, isFolder },
});
return
;
});
export interface VirtualizedTreeItemData {
flattenedItems: FlattenedTreeItem[];
collapsedIds: Set;
selectedItemIds: Set;
editingFolderId: string | null;
folderChildCounts: Map;
itemSeparatorInfo: Map;
visibleItemIds: Set;
searchTerm: string;
metricsMap: Map;
columnsMap: Map;
activeId: UniqueIdentifier | null;
draggedFolderChildIds: Set;
forbiddenDropFolderIds: Set;
currentDropTargetId: string | null;
onToggleCollapse: (id: string) => void;
onSelect: (id: string, selected: boolean, shiftKey?: boolean) => void;
onStartEdit: (id: string) => void;
onFinishEdit: (id: string, newName: string) => void;
}
// Inner component that receives state as props for proper memoization
interface TreeItemWrapperProps {
item: FlattenedTreeItem;
style: CSSProperties;
isFolder: boolean;
isCollapsed: boolean;
isSelected: boolean;
isEditing: boolean;
showEmptyState: boolean;
separatorType?: 'visible' | 'transparent';
isForbiddenDrop: boolean;
isDropTarget: boolean;
metric?: Metric;
column?: ColumnMeta;
onToggleCollapse?: (id: string) => void;
onSelect?: (id: string, selected: boolean, shiftKey?: boolean) => void;
onStartEdit?: (id: string) => void;
onFinishEdit?: (id: string, newName: string) => void;
}
const TreeItemWrapper = memo(function TreeItemWrapper({
item,
style,
isFolder,
isCollapsed,
isSelected,
isEditing,
showEmptyState,
separatorType,
isForbiddenDrop,
isDropTarget,
metric,
column,
onToggleCollapse,
onSelect,
onStartEdit,
onFinishEdit,
}: TreeItemWrapperProps) {
return (
);
});
function VirtualizedTreeItemComponent({
index,
style,
data,
}: ListChildComponentProps) {
const {
flattenedItems,
collapsedIds,
selectedItemIds,
editingFolderId,
folderChildCounts,
itemSeparatorInfo,
visibleItemIds,
searchTerm,
metricsMap,
columnsMap,
activeId,
draggedFolderChildIds,
forbiddenDropFolderIds,
currentDropTargetId,
onToggleCollapse,
onSelect,
onStartEdit,
onFinishEdit,
} = data;
const item = flattenedItems[index];
if (!item) {
return null;
}
const isFolder = item.type === FoldersEditorItemType.Folder;
// Hide items that don't match search (unless they're folders)
if (!isFolder && searchTerm && !visibleItemIds.has(item.uuid)) {
return null;
}
// Render invisible placeholder for active dragged item - keeps droppable area
// for horizontal drag depth changes while visual is in DragOverlay
if (activeId === item.uuid) {
return (
);
}
// Hidden descendants of the dragged folder — not droppable.
// handleDragEnd uses lastValidOverIdRef when dropping in this dead zone.
if (draggedFolderChildIds.has(item.uuid)) {
return ;
}
const childCount = isFolder ? (folderChildCounts.get(item.uuid) ?? 0) : 0;
const showEmptyState = isFolder && childCount === 0;
// isForbiddenDrop is calculated from props (changes when dragged items change)
const isForbiddenDrop = isFolder && forbiddenDropFolderIds.has(item.uuid);
// isDropTarget indicates this folder is the current drop target
const isDropTarget = isFolder && currentDropTargetId === item.uuid;
return (
);
}
export const VirtualizedTreeItem = memo(VirtualizedTreeItemComponent);