This is an automated email from the ASF dual-hosted git repository. msyavuz pushed a commit to branch msyavuz/fix/dataset-folders-improved-search in repository https://gitbox.apache.org/repos/asf/superset.git
commit b0b5cf23d0cd52983f9ef575e2b7760761088095 Author: Mehmet Salih Yavuz <[email protected]> AuthorDate: Mon Feb 23 15:15:57 2026 +0300 fix(Dataset Folders): improve search-collapse --- .../components/Datasource/FoldersEditor/index.tsx | 193 ++++++++++++++++++--- 1 file changed, 169 insertions(+), 24 deletions(-) diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx b/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx index 7325569c50c..01a9c271a75 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx +++ b/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx @@ -86,48 +86,184 @@ export default function FoldersEditor({ const [searchTerm, setSearchTerm] = useState(''); const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set()); const [editingFolderId, setEditingFolderId] = useState<string | null>(null); + const [preSearchCollapsedIds, setPreSearchCollapsedIds] = + useState<Set<string> | null>(null); const [showResetConfirm, setShowResetConfirm] = useState(false); const sensors = useSensors(useSensor(PointerSensor, pointerSensorOptions)); const fullFlattenedItems = useMemo(() => flattenTree(items), [items]); + // Track which folders contain matching items during search + const { visibleItemIds, searchExpandedFolderIds, foldersWithMatches } = + useMemo(() => { + if (!searchTerm) { + const allIds = new Set<string>(); + metrics.forEach(m => allIds.add(m.uuid)); + columns.forEach(c => allIds.add(c.uuid)); + return { + visibleItemIds: allIds, + searchExpandedFolderIds: new Set<string>(), + foldersWithMatches: new Set<string>(), + }; + } + + const allItems = [...metrics, ...columns]; + const matchingItemIds = filterItemsBySearch(searchTerm, allItems); + const expandedFolders = new Set<string>(); + const matchingFolders = new Set<string>(); + const lowerSearch = searchTerm.toLowerCase(); + + // Helper to check if folder title matches search + const folderMatches = (folder: TreeItemType): boolean => + folder.type === FoldersEditorItemType.Folder && + folder.name?.toLowerCase().includes(lowerSearch); + + // Helper to recursively check if a folder contains matching items + const folderContainsMatches = (folder: TreeItemType): boolean => { + if (folder.type !== FoldersEditorItemType.Folder) return false; + + // If folder name matches, it contains matches + if (folderMatches(folder)) { + return true; + } + + // Check direct children + if ( + folder.children?.some(child => { + if (child.type === FoldersEditorItemType.Folder) { + return folderContainsMatches(child); + } + return matchingItemIds.has(child.uuid); + }) + ) { + return true; + } + + return false; + }; + + // Helper to get all item IDs in a folder + const getAllItemsInFolder = ( + folder: TreeItemType, + itemSet: Set<string>, + ) => { + if ('children' in folder && folder.children) { + folder.children.forEach((child: TreeItemType) => { + if (child.type === FoldersEditorItemType.Folder) { + getAllItemsInFolder(child, itemSet); + } else { + itemSet.add(child.uuid); + } + }); + } + }; + + // Process each folder to determine which should expand and which items to show + const finalVisibleItems = new Set<string>(matchingItemIds); + + items.forEach(item => { + if (item.type === FoldersEditorItemType.Folder) { + if (folderMatches(item)) { + // Folder title matches - expand and show all children + expandedFolders.add(item.uuid); + matchingFolders.add(item.uuid); + getAllItemsInFolder(item, finalVisibleItems); + + // Recursively expand all subfolders + const expandAllSubfolders = (folder: TreeItemType) => { + if ('children' in folder && folder.children) { + folder.children.forEach((child: TreeItemType) => { + if (child.type === FoldersEditorItemType.Folder) { + expandedFolders.add(child.uuid); + matchingFolders.add(child.uuid); + expandAllSubfolders(child); + } + }); + } + }; + expandAllSubfolders(item); + } else if (folderContainsMatches(item)) { + // Folder contains matching items - expand it + expandedFolders.add(item.uuid); + matchingFolders.add(item.uuid); + + // Recursively expand subfolders that contain matches + const expandMatchingSubfolders = (folder: TreeItemType) => { + if ('children' in folder && folder.children) { + folder.children.forEach((child: TreeItemType) => { + if ( + child.type === FoldersEditorItemType.Folder && + folderContainsMatches(child) + ) { + expandedFolders.add(child.uuid); + matchingFolders.add(child.uuid); + expandMatchingSubfolders(child); + } + }); + } + }; + expandMatchingSubfolders(item); + } + } + }); + + return { + visibleItemIds: finalVisibleItems, + searchExpandedFolderIds: expandedFolders, + foldersWithMatches: matchingFolders, + }; + }, [searchTerm, metrics, columns, items]); + const collapsedFolderIds = useMemo(() => { const result: UniqueIdentifier[] = []; for (const { uuid, type, children } of fullFlattenedItems) { - if ( - type === FoldersEditorItemType.Folder && - collapsedIds.has(uuid) && - children?.length - ) { - result.push(uuid); + if (type === FoldersEditorItemType.Folder && children?.length) { + // During search, use search-expanded folders + if (searchTerm) { + if (!searchExpandedFolderIds.has(uuid)) { + result.push(uuid); + } + } else { + // Normal collapse state when not searching + if (collapsedIds.has(uuid)) { + result.push(uuid); + } + } } } return result; - }, [fullFlattenedItems, collapsedIds]); + }, [fullFlattenedItems, collapsedIds, searchTerm, searchExpandedFolderIds]); const computeFlattenedItems = useCallback( - (activeId: UniqueIdentifier | null) => - removeChildrenOf( - fullFlattenedItems, + (activeId: UniqueIdentifier | null) => { + // During search, filter out folders that don't match + let itemsToProcess = fullFlattenedItems; + if (searchTerm && foldersWithMatches) { + itemsToProcess = fullFlattenedItems.filter(item => { + if (item.type === FoldersEditorItemType.Folder) { + return foldersWithMatches.has(item.uuid); + } + return visibleItemIds.has(item.uuid); + }); + } + + return removeChildrenOf( + itemsToProcess, activeId != null ? [activeId, ...collapsedFolderIds] : collapsedFolderIds, - ), - [fullFlattenedItems, collapsedFolderIds], + ); + }, + [ + fullFlattenedItems, + collapsedFolderIds, + searchTerm, + foldersWithMatches, + visibleItemIds, + ], ); - const visibleItemIds = useMemo(() => { - if (!searchTerm) { - const allIds = new Set<string>(); - metrics.forEach(m => allIds.add(m.uuid)); - columns.forEach(c => allIds.add(c.uuid)); - return allIds; - } - const allItems = [...metrics, ...columns]; - return filterItemsBySearch(searchTerm, allItems); - }, [searchTerm, metrics, columns]); - const metricsMap = useMemo( () => new Map(metrics.map(m => [m.uuid, m])), [metrics], @@ -162,9 +298,18 @@ export default function FoldersEditor({ const debouncedSearch = useCallback( debounce((term: string) => { + // Save collapsed state before search starts + if (!searchTerm && term) { + setPreSearchCollapsedIds(new Set(collapsedIds)); + } + // Restore collapsed state when search is cleared + if (searchTerm && !term && preSearchCollapsedIds) { + setCollapsedIds(preSearchCollapsedIds); + setPreSearchCollapsedIds(null); + } setSearchTerm(term); }, 300), - [], + [searchTerm, collapsedIds, preSearchCollapsedIds], ); const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
