/** * 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);