This is an automated email from the ASF dual-hosted git repository. kgabryje pushed a commit to branch folders in repository https://gitbox.apache.org/repos/asf/superset.git
commit ec8fe259d4a66d1be4f84fca8960df90d6e3bda2 Author: Kamil Gabryjelski <[email protected]> AuthorDate: Thu Jan 29 17:26:35 2026 +0100 Code cleanup --- .../FoldersEditor/FoldersEditor.test.tsx | 11 - .../FoldersEditor/folderOperations.test.ts | 536 +-------------------- .../Datasource/FoldersEditor/folderOperations.ts | 192 -------- .../Datasource/FoldersEditor/folderValidation.ts | 129 ----- .../components/Datasource/FoldersEditor/sensors.ts | 8 - .../Datasource/FoldersEditor/treeUtils.test.ts | 281 ----------- .../Datasource/FoldersEditor/treeUtils.ts | 86 ---- .../components/Datasource/FoldersEditor/types.ts | 31 -- 8 files changed, 1 insertion(+), 1273 deletions(-) diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx b/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx index f1e40e3d20e..fbf3080cd14 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx +++ b/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx @@ -129,7 +129,6 @@ const defaultProps = { metrics: mockMetrics, columns: mockColumns, onChange: jest.fn(), - isEditMode: true, }; test('renders FoldersEditor with folders', () => { @@ -155,16 +154,6 @@ test('renders action buttons when in edit mode', () => { expect(screen.getByText('Reset all folders to default')).toBeInTheDocument(); }); -test('renders action buttons (always enabled regardless of isEditMode)', () => { - renderEditor(<FoldersEditor {...defaultProps} isEditMode={false} />); - - // Buttons should be enabled even when isEditMode is false - // The Folders feature is always editable when the tab is visible - expect(screen.getByText('Add folder')).toBeInTheDocument(); - expect(screen.getByText('Select all')).toBeInTheDocument(); - expect(screen.getByText('Reset all folders to default')).toBeInTheDocument(); -}); - test('adds a new folder when Add folder button is clicked', async () => { renderEditor(<FoldersEditor {...defaultProps} />); diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.test.ts b/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.test.ts index 361249f328c..df7e1ea9280 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.test.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.test.ts @@ -26,26 +26,11 @@ import { } from './constants'; import { createFolder, - deleteFolder, resetToDefault, filterItemsBySearch, - renameFolder, - nestFolder, - reorderFolders, - moveItems, - cleanupFolders, - getAllSelectedItems, - areAllVisibleItemsSelected, ensureDefaultFolders, } from './folderOperations'; -import { - canDropItems, - canDropFolder, - getFolderDescendants, - findFolderById, - validateFolders, -} from './folderValidation'; -import { DatasourceFolder } from 'src/explore/components/DatasourcePanel/types'; +import { validateFolders } from './folderValidation'; import { FoldersEditorItemType } from '../types'; describe('folderUtils', () => { @@ -141,73 +126,6 @@ describe('folderUtils', () => { }); }); - describe('renameFolder', () => { - test('should rename folder correctly', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const result = renameFolder( - DEFAULT_METRICS_FOLDER_UUID, - 'Custom Metrics', - folders, - ); - - const renamedFolder = result.find( - f => f.uuid === DEFAULT_METRICS_FOLDER_UUID, - ); - expect(renamedFolder?.name).toBe('Custom Metrics'); - }); - }); - - describe('canDropItems', () => { - test('should allow dropping metrics in Metrics folder', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const result = canDropItems( - ['metric-1'], - DEFAULT_METRICS_FOLDER_UUID, - folders, - mockMetrics, - mockColumns, - ); - - expect(result).toBe(true); - }); - - test('should not allow dropping columns in Metrics folder', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const result = canDropItems( - ['column-1'], - DEFAULT_METRICS_FOLDER_UUID, - folders, - mockMetrics, - mockColumns, - ); - - expect(result).toBe(false); - }); - - test('should allow dropping any items in custom folders', () => { - const customFolder = createFolder('Custom Folder'); - const folders = [customFolder]; - - const metricResult = canDropItems( - ['metric-1'], - customFolder.uuid, - folders, - mockMetrics, - mockColumns, - ); - const columnResult = canDropItems( - ['column-1'], - customFolder.uuid, - folders, - mockMetrics, - mockColumns, - ); - - expect(metricResult).toBe(true); - expect(columnResult).toBe(true); - }); - }); - describe('isDefaultFolder', () => { test('should identify default folders by UUID', () => { expect(isDefaultFolder(DEFAULT_METRICS_FOLDER_UUID)).toBe(true); @@ -296,456 +214,4 @@ describe('folderUtils', () => { expect(result.find(f => f.name === 'Custom Folder')).toBeDefined(); }); }); - - describe('deleteFolder', () => { - test('should delete a folder by id', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const result = deleteFolder(DEFAULT_METRICS_FOLDER_UUID, folders); - - expect(result).toHaveLength(1); - expect( - result.find(f => f.uuid === DEFAULT_METRICS_FOLDER_UUID), - ).toBeUndefined(); - expect( - result.find(f => f.uuid === DEFAULT_COLUMNS_FOLDER_UUID), - ).toBeDefined(); - }); - - test('should delete nested folders', () => { - const parentFolder: DatasourceFolder = { - uuid: 'parent', - type: FoldersEditorItemType.Folder, - name: 'Parent', - children: [ - { - uuid: 'child', - type: FoldersEditorItemType.Folder, - name: 'Child', - children: [], - } as DatasourceFolder, - ], - }; - const folders = [parentFolder]; - const result = deleteFolder('child', folders); - - expect(result).toHaveLength(1); - expect( - (result[0].children as DatasourceFolder[]).find( - c => c.uuid === 'child', - ), - ).toBeUndefined(); - }); - - test('should return unchanged array if folder not found', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const result = deleteFolder('nonexistent', folders); - - expect(result).toHaveLength(2); - }); - }); - - describe('nestFolder', () => { - test('should nest a folder inside another folder', () => { - const folder1: DatasourceFolder = { - uuid: 'folder1', - type: FoldersEditorItemType.Folder, - name: 'Folder 1', - children: [], - }; - const folder2: DatasourceFolder = { - uuid: 'folder2', - type: FoldersEditorItemType.Folder, - name: 'Folder 2', - children: [], - }; - const folders = [folder1, folder2]; - - const result = nestFolder('folder2', 'folder1', folders); - - expect(result).toHaveLength(1); - expect(result[0].uuid).toBe('folder1'); - expect(result[0].children).toHaveLength(1); - expect((result[0].children as DatasourceFolder[])[0].uuid).toBe( - 'folder2', - ); - }); - - test('should return unchanged if folder to move not found', () => { - const folder1: DatasourceFolder = { - uuid: 'folder1', - type: FoldersEditorItemType.Folder, - name: 'Folder 1', - children: [], - }; - const folders = [folder1]; - - const result = nestFolder('nonexistent', 'folder1', folders); - - expect(result).toEqual(folders); - }); - }); - - describe('reorderFolders', () => { - test('should reorder folders at root level', () => { - const folder1: DatasourceFolder = { - uuid: 'folder1', - type: FoldersEditorItemType.Folder, - name: 'Folder 1', - children: [], - }; - const folder2: DatasourceFolder = { - uuid: 'folder2', - type: FoldersEditorItemType.Folder, - name: 'Folder 2', - children: [], - }; - const folder3: DatasourceFolder = { - uuid: 'folder3', - type: FoldersEditorItemType.Folder, - name: 'Folder 3', - children: [], - }; - const folders = [folder1, folder2, folder3]; - - const result = reorderFolders('folder3', 0, folders); - - expect(result[0].uuid).toBe('folder3'); - expect(result[1].uuid).toBe('folder1'); - expect(result[2].uuid).toBe('folder2'); - }); - - test('should return unchanged if folder not found', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const result = reorderFolders('nonexistent', 0, folders); - - expect(result).toEqual(folders); - }); - }); - - describe('moveItems', () => { - test('should move items from one folder to another', () => { - const folders: DatasourceFolder[] = [ - { - uuid: 'folder1', - type: FoldersEditorItemType.Folder, - name: 'Folder 1', - children: [ - { - uuid: 'metric-1', - type: FoldersEditorItemType.Metric, - name: 'Metric 1', - }, - { - uuid: 'metric-2', - type: FoldersEditorItemType.Metric, - name: 'Metric 2', - }, - ], - }, - { - uuid: 'folder2', - type: FoldersEditorItemType.Folder, - name: 'Folder 2', - children: [], - }, - ]; - - const result = moveItems(['metric-1'], 'folder2', folders); - - expect(result[0].children).toHaveLength(1); - expect(result[1].children).toHaveLength(1); - expect(result[1].children![0].uuid).toBe('metric-1'); - }); - - test('should move multiple items at once', () => { - const folders: DatasourceFolder[] = [ - { - uuid: 'folder1', - type: FoldersEditorItemType.Folder, - name: 'Folder 1', - children: [ - { - uuid: 'metric-1', - type: FoldersEditorItemType.Metric, - name: 'Metric 1', - }, - { - uuid: 'metric-2', - type: FoldersEditorItemType.Metric, - name: 'Metric 2', - }, - ], - }, - { - uuid: 'folder2', - type: FoldersEditorItemType.Folder, - name: 'Folder 2', - children: [], - }, - ]; - - const result = moveItems(['metric-1', 'metric-2'], 'folder2', folders); - - expect(result[0].children).toHaveLength(0); - expect(result[1].children).toHaveLength(2); - }); - }); - - describe('canDropFolder', () => { - test('should prevent dropping folder on itself', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const result = canDropFolder('folder1', 'folder1', folders); - - expect(result).toBe(false); - }); - - test('should prevent dropping folder on its descendants', () => { - const folders: DatasourceFolder[] = [ - { - uuid: 'parent', - type: FoldersEditorItemType.Folder, - name: 'Parent', - children: [ - { - uuid: 'child', - type: FoldersEditorItemType.Folder, - name: 'Child', - children: [ - { - uuid: 'grandchild', - type: FoldersEditorItemType.Folder, - name: 'Grandchild', - children: [], - } as DatasourceFolder, - ], - } as DatasourceFolder, - ], - }, - ]; - - expect(canDropFolder('parent', 'child', folders)).toBe(false); - expect(canDropFolder('parent', 'grandchild', folders)).toBe(false); - }); - - test('should prevent dropping default folders into other folders', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const customFolder = createFolder('Custom'); - folders.push(customFolder); - - expect( - canDropFolder(DEFAULT_METRICS_FOLDER_UUID, customFolder.uuid, folders), - ).toBe(false); - expect( - canDropFolder(DEFAULT_COLUMNS_FOLDER_UUID, customFolder.uuid, folders), - ).toBe(false); - }); - - test('should allow valid folder drops', () => { - const folder1: DatasourceFolder = { - uuid: 'folder1', - type: FoldersEditorItemType.Folder, - name: 'Folder 1', - children: [], - }; - const folder2: DatasourceFolder = { - uuid: 'folder2', - type: FoldersEditorItemType.Folder, - name: 'Folder 2', - children: [], - }; - const folders = [folder1, folder2]; - - expect(canDropFolder('folder1', 'folder2', folders)).toBe(true); - }); - }); - - describe('getFolderDescendants', () => { - test('should return all descendant folder IDs', () => { - const folders: DatasourceFolder[] = [ - { - uuid: 'parent', - type: FoldersEditorItemType.Folder, - name: 'Parent', - children: [ - { - uuid: 'child1', - type: FoldersEditorItemType.Folder, - name: 'Child 1', - children: [ - { - uuid: 'grandchild', - type: FoldersEditorItemType.Folder, - name: 'Grandchild', - children: [], - } as DatasourceFolder, - ], - } as DatasourceFolder, - { - uuid: 'child2', - type: FoldersEditorItemType.Folder, - name: 'Child 2', - children: [], - } as DatasourceFolder, - ], - }, - ]; - - const descendants = getFolderDescendants('parent', folders); - - expect(descendants).toContain('child1'); - expect(descendants).toContain('child2'); - expect(descendants).toContain('grandchild'); - expect(descendants).toHaveLength(3); - }); - - test('should return empty array for non-existent folder', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const descendants = getFolderDescendants('nonexistent', folders); - - expect(descendants).toHaveLength(0); - }); - }); - - describe('findFolderById', () => { - test('should find folder at root level', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const found = findFolderById(DEFAULT_METRICS_FOLDER_UUID, folders); - - expect(found).toBeDefined(); - expect(found?.uuid).toBe(DEFAULT_METRICS_FOLDER_UUID); - }); - - test('should find nested folder', () => { - const folders: DatasourceFolder[] = [ - { - uuid: 'parent', - type: FoldersEditorItemType.Folder, - name: 'Parent', - children: [ - { - uuid: 'child', - type: FoldersEditorItemType.Folder, - name: 'Child', - children: [], - } as DatasourceFolder, - ], - }, - ]; - - const found = findFolderById('child', folders); - - expect(found).toBeDefined(); - expect(found?.uuid).toBe('child'); - }); - - test('should return null for non-existent folder', () => { - const folders = resetToDefault(mockMetrics, mockColumns); - const found = findFolderById('nonexistent', folders); - - expect(found).toBeNull(); - }); - }); - - describe('cleanupFolders', () => { - test('should remove empty non-default folders', () => { - const folders: DatasourceFolder[] = [ - { - uuid: 'empty-folder', - type: FoldersEditorItemType.Folder, - name: 'Empty Folder', - children: [], - }, - { - uuid: 'non-empty-folder', - type: FoldersEditorItemType.Folder, - name: 'Non-Empty Folder', - children: [ - { - uuid: 'metric-1', - type: FoldersEditorItemType.Metric, - name: 'Metric 1', - }, - ], - }, - ]; - - const result = cleanupFolders(folders); - - expect(result).toHaveLength(1); - expect(result[0].uuid).toBe('non-empty-folder'); - }); - - test('should preserve empty default folders', () => { - const folders: DatasourceFolder[] = [ - { - uuid: DEFAULT_METRICS_FOLDER_UUID, - type: FoldersEditorItemType.Folder, - name: 'Metrics', - children: [], - }, - { - uuid: DEFAULT_COLUMNS_FOLDER_UUID, - type: FoldersEditorItemType.Folder, - name: 'Columns', - children: [], - }, - ]; - - const result = cleanupFolders(folders); - - expect(result).toHaveLength(2); - }); - }); - - describe('getAllSelectedItems', () => { - test('should return only visible items that are selected', () => { - const selectedItemIds = new Set(['item1', 'item2', 'item3']); - const visibleItemIds = ['item1', 'item3', 'item4']; - - const result = getAllSelectedItems(selectedItemIds, visibleItemIds); - - expect(result).toHaveLength(2); - expect(result).toContain('item1'); - expect(result).toContain('item3'); - expect(result).not.toContain('item2'); - }); - - test('should return empty array if no items are selected', () => { - const selectedItemIds = new Set<string>(); - const visibleItemIds = ['item1', 'item2']; - - const result = getAllSelectedItems(selectedItemIds, visibleItemIds); - - expect(result).toHaveLength(0); - }); - }); - - describe('areAllVisibleItemsSelected', () => { - test('should return true when all visible items are selected', () => { - const selectedItemIds = new Set(['item1', 'item2', 'item3']); - const visibleItemIds = ['item1', 'item2']; - - expect(areAllVisibleItemsSelected(selectedItemIds, visibleItemIds)).toBe( - true, - ); - }); - - test('should return false when some visible items are not selected', () => { - const selectedItemIds = new Set(['item1']); - const visibleItemIds = ['item1', 'item2']; - - expect(areAllVisibleItemsSelected(selectedItemIds, visibleItemIds)).toBe( - false, - ); - }); - - test('should return false when visible items is empty', () => { - const selectedItemIds = new Set(['item1']); - const visibleItemIds: string[] = []; - - expect(areAllVisibleItemsSelected(selectedItemIds, visibleItemIds)).toBe( - false, - ); - }); - }); }); diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.ts b/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.ts index 4543c9cecd6..852de41f7f9 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/folderOperations.ts @@ -33,7 +33,6 @@ import { FoldersEditorItemType } from '../types'; import { DEFAULT_METRICS_FOLDER_UUID, DEFAULT_COLUMNS_FOLDER_UUID, - isDefaultFolder, } from './constants'; export const createFolder = (name: string): DatasourceFolder => ({ @@ -43,159 +42,6 @@ export const createFolder = (name: string): DatasourceFolder => ({ children: [], }); -export const deleteFolder = ( - folderId: string, - folders: DatasourceFolder[], -): DatasourceFolder[] => { - const deleteFolderRecursive = ( - items: DatasourceFolder[], - ): DatasourceFolder[] => - items - .filter(item => item.uuid !== folderId) - .map(item => ({ - ...item, - children: item.children - ? deleteFolderRecursive(item.children as DatasourceFolder[]) - : item.children, - })); - - return deleteFolderRecursive(folders); -}; - -export const renameFolder = ( - folderId: string, - newName: string, - folders: DatasourceFolder[], -): DatasourceFolder[] => { - const renameFolderRecursive = ( - items: DatasourceFolder[], - ): DatasourceFolder[] => - items.map(item => { - if (item.uuid === folderId) { - return { ...item, name: newName }; - } - if (item.children && item.type === 'folder') { - return { - ...item, - children: renameFolderRecursive(item.children as DatasourceFolder[]), - }; - } - return item; - }); - - return renameFolderRecursive(folders); -}; - -export const nestFolder = ( - folderId: string, - parentId: string, - folders: DatasourceFolder[], -): DatasourceFolder[] => { - let folderToMove: DatasourceFolder | null = null; - - const findAndRemoveFolder = (items: DatasourceFolder[]): DatasourceFolder[] => - items - .filter(item => { - if (item.uuid === folderId) { - folderToMove = item; - return false; - } - return true; - }) - .map(item => ({ - ...item, - children: - item.children && item.type === 'folder' - ? findAndRemoveFolder(item.children as DatasourceFolder[]) - : item.children, - })); - - const foldersWithoutTarget = findAndRemoveFolder(folders); - - if (!folderToMove) return folders; - - const addToParent = (items: DatasourceFolder[]): DatasourceFolder[] => - items.map(item => { - if (item.uuid === parentId) { - return { - ...item, - children: [...(item.children || []), folderToMove!], - }; - } - if (item.children && item.type === 'folder') { - return { - ...item, - children: addToParent(item.children as DatasourceFolder[]), - }; - } - return item; - }); - - return addToParent(foldersWithoutTarget); -}; - -export const reorderFolders = ( - folderId: string, - newIndex: number, - folders: DatasourceFolder[], -): DatasourceFolder[] => { - const currentIndex = folders.findIndex(f => f.uuid === folderId); - if (currentIndex === -1) return folders; - - const result = [...folders]; - const [removed] = result.splice(currentIndex, 1); - result.splice(newIndex, 0, removed); - return result; -}; - -export const moveItems = ( - itemIds: string[], - targetFolderId: string, - folders: DatasourceFolder[], -): DatasourceFolder[] => { - const itemsToMove: Array<{ - type: FoldersEditorItemType.Metric | FoldersEditorItemType.Column; - uuid: string; - name: string; - }> = []; - - const removeItems = (items: DatasourceFolder[]): DatasourceFolder[] => - items.map(folder => ({ - ...folder, - children: folder.children - ? folder.children.filter(child => { - if ( - child.type !== FoldersEditorItemType.Folder && - itemIds.includes(child.uuid) - ) { - itemsToMove.push({ - type: child.type, - uuid: child.uuid, - name: child.name || '', - }); - return false; - } - return true; - }) - : folder.children, - })); - - const foldersWithoutItems = removeItems(folders); - - const addItems = (items: DatasourceFolder[]): DatasourceFolder[] => - items.map(folder => { - if (folder.uuid === targetFolderId) { - return { - ...folder, - children: [...(folder.children || []), ...itemsToMove], - }; - } - return folder; - }); - - return addItems(foldersWithoutItems); -}; - export const resetToDefault = ( metrics: Metric[], columns: ColumnMeta[], @@ -242,44 +88,6 @@ export const filterItemsBySearch = ( return matchingIds; }; -export const cleanupFolders = ( - folders: DatasourceFolder[], -): DatasourceFolder[] => { - const cleanRecursive = (items: DatasourceFolder[]): DatasourceFolder[] => - items - .filter(folder => { - if (isDefaultFolder(folder.uuid)) { - return true; - } - return folder.children && folder.children.length > 0; - }) - .map(folder => ({ - ...folder, - children: - folder.children && folder.type === 'folder' - ? cleanRecursive( - folder.children.filter( - c => c.type === 'folder', - ) as DatasourceFolder[], - ) - : folder.children, - })); - - return cleanRecursive(folders); -}; - -export const getAllSelectedItems = ( - selectedItemIds: Set<string>, - visibleItemIds: string[], -): string[] => visibleItemIds.filter(id => selectedItemIds.has(id)); - -export const areAllVisibleItemsSelected = ( - selectedItemIds: Set<string>, - visibleItemIds: string[], -): boolean => - visibleItemIds.length > 0 && - visibleItemIds.every(id => selectedItemIds.has(id)); - /** * Enrich folder children with names from metrics/columns arrays * API returns {uuid} only, we need to add {type, name} for display diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/folderValidation.ts b/superset-frontend/src/components/Datasource/FoldersEditor/folderValidation.ts index 1e49d86407c..a9430da322c 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/folderValidation.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/folderValidation.ts @@ -22,142 +22,13 @@ * Determines what actions are allowed based on folder structure and types. */ -import { Metric, ColumnMeta } from '@superset-ui/chart-controls'; import { t } from '@apache-superset/core'; import { DatasourceFolder } from 'src/explore/components/DatasourcePanel/types'; -import { UniqueIdentifier } from '@dnd-kit/core'; -import { FoldersEditorItemType } from '../types'; import { - FlattenedTreeItem, - TreeItem, ValidationResult, DEFAULT_METRICS_FOLDER_UUID, DEFAULT_COLUMNS_FOLDER_UUID, - isDefaultFolder, } from './constants'; -import { getDescendantIds } from './treeUtils'; - -export const canAcceptDrop = ( - targetFolder: FlattenedTreeItem, - draggedItems: FlattenedTreeItem[], -): boolean => { - const isDefaultMetricsFolder = - targetFolder.uuid === DEFAULT_METRICS_FOLDER_UUID; - const isDefaultColumnsFolder = - targetFolder.uuid === DEFAULT_COLUMNS_FOLDER_UUID; - - if (isDefaultMetricsFolder) { - return draggedItems.every( - item => item.type === FoldersEditorItemType.Metric, - ); - } - - if (isDefaultColumnsFolder) { - return draggedItems.every( - item => item.type === FoldersEditorItemType.Column, - ); - } - - return true; -}; - -export const canNestFolder = ( - items: TreeItem[], - movingFolderId: string, - targetFolderId: string, -): boolean => { - if (movingFolderId === targetFolderId) { - return false; - } - - const descendants = getDescendantIds(items, movingFolderId); - return !descendants.includes(targetFolderId); -}; - -export const canDropFolder = ( - folderId: UniqueIdentifier, - targetId: UniqueIdentifier, - folders: DatasourceFolder[], -): boolean => { - if (folderId === targetId) return false; - - const descendants = getFolderDescendants(folderId, folders); - if (descendants.includes(targetId)) { - return false; - } - - const draggedFolder = findFolderById(folderId, folders); - if (draggedFolder && isDefaultFolder(draggedFolder.uuid)) { - return false; - } - - return true; -}; - -export const canDropItems = ( - itemIds: string[], - targetFolderId: string, - folders: DatasourceFolder[], - metrics: Metric[], - columns: ColumnMeta[], -): boolean => { - const targetFolder = findFolderById(targetFolderId, folders); - if (!targetFolder) return false; - - if (targetFolder.uuid === DEFAULT_METRICS_FOLDER_UUID) { - return itemIds.every(id => metrics.some(m => m.uuid === id)); - } - - if (targetFolder.uuid === DEFAULT_COLUMNS_FOLDER_UUID) { - return itemIds.every(id => columns.some(c => c.uuid === id)); - } - - return true; -}; - -export const getFolderDescendants = ( - folderId: UniqueIdentifier, - folders: DatasourceFolder[], -): UniqueIdentifier[] => { - const descendants: UniqueIdentifier[] = []; - - const collectDescendants = (folder: DatasourceFolder) => { - if (folder.children) { - folder.children.forEach(child => { - if (child.type === 'folder') { - descendants.push(child.uuid); - collectDescendants(child as DatasourceFolder); - } - }); - } - }; - - const folder = findFolderById(folderId, folders); - if (folder) { - collectDescendants(folder); - } - - return descendants; -}; - -export const findFolderById = ( - folderId: UniqueIdentifier, - folders: DatasourceFolder[], -): DatasourceFolder | null => { - for (const folder of folders) { - if (folder.uuid === folderId) { - return folder; - } - if (folder.children) { - const found = findFolderById( - folderId, - folder.children.filter(c => c.type === 'folder') as DatasourceFolder[], - ); - if (found) return found; - } - } - return null; -}; export const validateFolders = ( folders: DatasourceFolder[], diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/sensors.ts b/superset-frontend/src/components/Datasource/FoldersEditor/sensors.ts index 7b829a4a576..3c8cdc467e9 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/sensors.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/sensors.ts @@ -18,7 +18,6 @@ */ import { - PointerSensor, PointerSensorOptions, MeasuringConfiguration, MeasuringStrategy, @@ -30,8 +29,6 @@ export const pointerSensorOptions: PointerSensorOptions = { }, }; -export const createPointerSensor = () => PointerSensor; - // Use BeforeDragging strategy to measure items once at drag start rather than continuously. // This is critical for virtualized lists where items get unmounted during scroll. // MeasuringStrategy.Always causes issues because dnd-kit loses track of items @@ -48,8 +45,3 @@ export const measuringConfig: MeasuringConfiguration = { export const autoScrollConfig = { enabled: false, }; - -export const sensorConfig = { - PointerSensor: createPointerSensor(), - options: pointerSensorOptions, -}; diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.test.ts b/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.test.ts index 852a475bf5e..c6fdfa444b7 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.test.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.test.ts @@ -21,20 +21,14 @@ import { TreeItem, FlattenedTreeItem, DRAG_INDENTATION_WIDTH, - DEFAULT_COLUMNS_FOLDER_UUID, - DEFAULT_METRICS_FOLDER_UUID, } from './constants'; import { flattenTree, buildTree, - findItemDeep, removeChildrenOf, - getChildCount, serializeForAPI, - getDescendantIds, getProjection, } from './treeUtils'; -import { canAcceptDrop, canNestFolder } from './folderValidation'; import { FoldersEditorItemType } from '../types'; const createMetricItem = (uuid: string, name: string): TreeItem => ({ @@ -170,41 +164,6 @@ test('buildTree handles orphan items by placing them at root', () => { expect(tree[0].uuid).toBe('metric1'); }); -test('findItemDeep finds item at root level', () => { - const tree: TreeItem[] = [ - createFolderItem('folder1', 'Folder 1', []), - createFolderItem('folder2', 'Folder 2', []), - ]; - - const found = findItemDeep(tree, 'folder2'); - - expect(found).toBeDefined(); - expect(found?.uuid).toBe('folder2'); -}); - -test('findItemDeep finds deeply nested item', () => { - const tree: TreeItem[] = [ - createFolderItem('folder1', 'Folder 1', [ - createFolderItem('folder2', 'Folder 2', [ - createMetricItem('metric1', 'Metric 1'), - ]), - ]), - ]; - - const found = findItemDeep(tree, 'metric1'); - - expect(found).toBeDefined(); - expect(found?.uuid).toBe('metric1'); -}); - -test('findItemDeep returns undefined for non-existent item', () => { - const tree: TreeItem[] = [createFolderItem('folder1', 'Folder 1', [])]; - - const found = findItemDeep(tree, 'nonexistent'); - - expect(found).toBeUndefined(); -}); - test('removeChildrenOf filters out children of specified parents', () => { const items: FlattenedTreeItem[] = [ { @@ -284,56 +243,6 @@ test('removeChildrenOf recursively removes nested children when parent has child expect(filtered[0].uuid).toBe('folder1'); }); -test('getChildCount returns correct count for folder', () => { - const tree: TreeItem[] = [ - createFolderItem('folder1', 'Folder 1', [ - createMetricItem('metric1', 'Metric 1'), - createMetricItem('metric2', 'Metric 2'), - createMetricItem('metric3', 'Metric 3'), - ]), - ]; - - const count = getChildCount(tree, 'folder1'); - - expect(count).toBe(3); -}); - -test('getChildCount includes nested children', () => { - const tree: TreeItem[] = [ - createFolderItem('folder1', 'Folder 1', [ - createFolderItem('folder2', 'Folder 2', [ - createMetricItem('metric1', 'Metric 1'), - createMetricItem('metric2', 'Metric 2'), - ]), - ]), - ]; - - const count = getChildCount(tree, 'folder1'); - - // folder2 + metric1 + metric2 = 3 - expect(count).toBe(3); -}); - -test('getChildCount returns 0 for non-folder items', () => { - const tree: TreeItem[] = [ - createFolderItem('folder1', 'Folder 1', [ - createMetricItem('metric1', 'Metric 1'), - ]), - ]; - - const count = getChildCount(tree, 'metric1'); - - expect(count).toBe(0); -}); - -test('getChildCount returns 0 for non-existent items', () => { - const tree: TreeItem[] = [createFolderItem('folder1', 'Folder 1', [])]; - - const count = getChildCount(tree, 'nonexistent'); - - expect(count).toBe(0); -}); - test('serializeForAPI excludes empty folders', () => { const tree: TreeItem[] = [ createFolderItem('folder1', 'Folder 1', []), @@ -400,196 +309,6 @@ test('serializeForAPI excludes nested empty folders', () => { }); }); -test('getDescendantIds returns all descendants of a folder', () => { - const tree: TreeItem[] = [ - createFolderItem('folder1', 'Folder 1', [ - createMetricItem('metric1', 'Metric 1'), - createFolderItem('folder2', 'Folder 2', [ - createColumnItem('column1', 'Column 1'), - ]), - ]), - ]; - - const descendants = getDescendantIds(tree, 'folder1'); - - expect(descendants).toContain('metric1'); - expect(descendants).toContain('folder2'); - expect(descendants).toContain('column1'); - expect(descendants).toHaveLength(3); -}); - -test('getDescendantIds returns empty array for non-folder items', () => { - const tree: TreeItem[] = [ - createFolderItem('folder1', 'Folder 1', [ - createMetricItem('metric1', 'Metric 1'), - ]), - ]; - - const descendants = getDescendantIds(tree, 'metric1'); - - expect(descendants).toHaveLength(0); -}); - -test('getDescendantIds returns empty array for non-existent items', () => { - const tree: TreeItem[] = [createFolderItem('folder1', 'Folder 1', [])]; - - const descendants = getDescendantIds(tree, 'nonexistent'); - - expect(descendants).toHaveLength(0); -}); - -test('canAcceptDrop allows metrics in default Metrics folder', () => { - const targetFolder: FlattenedTreeItem = { - uuid: DEFAULT_METRICS_FOLDER_UUID, - type: FoldersEditorItemType.Folder, - name: 'Metrics', - parentId: null, - depth: 0, - index: 0, - }; - const draggedItems: FlattenedTreeItem[] = [ - { - uuid: 'metric1', - type: FoldersEditorItemType.Metric, - name: 'Metric 1', - parentId: null, - depth: 1, - index: 0, - }, - ]; - - expect(canAcceptDrop(targetFolder, draggedItems)).toBe(true); -}); - -test('canAcceptDrop rejects columns in default Metrics folder', () => { - const targetFolder: FlattenedTreeItem = { - uuid: DEFAULT_METRICS_FOLDER_UUID, - type: FoldersEditorItemType.Folder, - name: 'Metrics', - parentId: null, - depth: 0, - index: 0, - }; - const draggedItems: FlattenedTreeItem[] = [ - { - uuid: 'column1', - type: FoldersEditorItemType.Column, - name: 'Column 1', - parentId: null, - depth: 1, - index: 0, - }, - ]; - - expect(canAcceptDrop(targetFolder, draggedItems)).toBe(false); -}); - -test('canAcceptDrop allows columns in default Columns folder', () => { - const targetFolder: FlattenedTreeItem = { - uuid: DEFAULT_COLUMNS_FOLDER_UUID, - type: FoldersEditorItemType.Folder, - name: 'Columns', - parentId: null, - depth: 0, - index: 0, - }; - const draggedItems: FlattenedTreeItem[] = [ - { - uuid: 'column1', - type: FoldersEditorItemType.Column, - name: 'Column 1', - parentId: null, - depth: 1, - index: 0, - }, - ]; - - expect(canAcceptDrop(targetFolder, draggedItems)).toBe(true); -}); - -test('canAcceptDrop rejects metrics in default Columns folder', () => { - const targetFolder: FlattenedTreeItem = { - uuid: DEFAULT_COLUMNS_FOLDER_UUID, - type: FoldersEditorItemType.Folder, - name: 'Columns', - parentId: null, - depth: 0, - index: 0, - }; - const draggedItems: FlattenedTreeItem[] = [ - { - uuid: 'metric1', - type: FoldersEditorItemType.Metric, - name: 'Metric 1', - parentId: null, - depth: 1, - index: 0, - }, - ]; - - expect(canAcceptDrop(targetFolder, draggedItems)).toBe(false); -}); - -test('canAcceptDrop allows any items in custom folders', () => { - const targetFolder: FlattenedTreeItem = { - uuid: 'custom-folder', - type: FoldersEditorItemType.Folder, - name: 'Custom Folder', - parentId: null, - depth: 0, - index: 0, - }; - const draggedItems: FlattenedTreeItem[] = [ - { - uuid: 'metric1', - type: FoldersEditorItemType.Metric, - name: 'Metric 1', - parentId: null, - depth: 1, - index: 0, - }, - { - uuid: 'column1', - type: FoldersEditorItemType.Column, - name: 'Column 1', - parentId: null, - depth: 1, - index: 1, - }, - ]; - - expect(canAcceptDrop(targetFolder, draggedItems)).toBe(true); -}); - -test('canNestFolder prevents folder from being nested inside itself', () => { - const tree: TreeItem[] = [createFolderItem('folder1', 'Folder 1', [])]; - - expect(canNestFolder(tree, 'folder1', 'folder1')).toBe(false); -}); - -test('canNestFolder prevents folder from being nested inside its descendants', () => { - const tree: TreeItem[] = [ - createFolderItem('parent', 'Parent', [ - createFolderItem('child', 'Child', [ - createFolderItem('grandchild', 'Grandchild', []), - ]), - ]), - ]; - - expect(canNestFolder(tree, 'parent', 'child')).toBe(false); - expect(canNestFolder(tree, 'parent', 'grandchild')).toBe(false); -}); - -test('canNestFolder allows valid nesting', () => { - const tree: TreeItem[] = [ - createFolderItem('folder1', 'Folder 1', []), - createFolderItem('folder2', 'Folder 2', []), - ]; - - expect(canNestFolder(tree, 'folder1', 'folder2')).toBe(true); - expect(canNestFolder(tree, 'folder2', 'folder1')).toBe(true); -}); - test('getProjection calculates correct depth when dragging down', () => { const items: FlattenedTreeItem[] = [ { diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts b/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts index 4938ea4dcbc..10dec0d28ab 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts @@ -247,30 +247,6 @@ export function buildTree(flattenedItems: FlattenedTreeItem[]): TreeItem[] { return root; } -export function findItemDeep( - items: TreeItem[], - itemId: UniqueIdentifier, -): TreeItem | undefined { - for (const item of items) { - if (item.uuid === itemId) { - return item; - } - - if ( - item.type === FoldersEditorItemType.Folder && - 'children' in item && - item.children?.length - ) { - const child = findItemDeep(item.children, itemId); - if (child) { - return child; - } - } - } - - return undefined; -} - export function removeChildrenOf( items: FlattenedTreeItem[], ids: UniqueIdentifier[], @@ -289,68 +265,6 @@ export function removeChildrenOf( }); } -function countChildren(items: TreeItem[], count: number = 0): number { - return items.reduce((acc, item) => { - if ( - item.type === FoldersEditorItemType.Folder && - 'children' in item && - item.children?.length - ) { - return countChildren(item.children, acc + 1); - } - return acc + 1; - }, count); -} - -export function getChildCount(items: TreeItem[], id: UniqueIdentifier): number { - const item = findItemDeep(items, id); - - if ( - item && - item.type === FoldersEditorItemType.Folder && - 'children' in item && - item.children - ) { - return countChildren(item.children); - } - - return 0; -} - -export function getDescendantIds( - items: TreeItem[], - folderId: string, -): string[] { - const folder = findItemDeep(items, folderId); - - if ( - !folder || - folder.type !== FoldersEditorItemType.Folder || - !('children' in folder) || - !folder.children - ) { - return []; - } - - const descendants: string[] = []; - - function collectIds(children: TreeItem[]) { - for (const child of children) { - descendants.push(child.uuid); - if ( - child.type === FoldersEditorItemType.Folder && - 'children' in child && - child.children - ) { - collectIds(child.children); - } - } - } - - collectIds(folder.children); - return descendants; -} - /** * Serialize tree for API. Empty folders are excluded. */ diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/types.ts b/superset-frontend/src/components/Datasource/FoldersEditor/types.ts index f729be3b836..06bf1bdd739 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/types.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/types.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { UniqueIdentifier } from '@dnd-kit/core'; import { Metric, ColumnMeta } from '@superset-ui/chart-controls'; import { DatasourceFolder } from 'src/explore/components/DatasourcePanel/types'; @@ -25,34 +24,4 @@ export interface FoldersEditorProps { metrics: Metric[]; columns: ColumnMeta[]; onChange: (folders: DatasourceFolder[]) => void; - isEditMode: boolean; -} - -export interface DragState { - activeId: UniqueIdentifier | null; - draggedType: 'folder' | 'item' | null; - draggedItems: string[]; - overId: UniqueIdentifier | null; -} - -export interface SortableItemProps { - id: string; - children: React.ReactNode; - isDragging?: boolean; - onSelect: (selected: boolean) => void; - isSelected: boolean; -} - -export interface SortableFolderProps { - folder: DatasourceFolder; - isExpanded: boolean; - isEditing: boolean; - onToggle: () => void; - onEdit: () => void; - onNameChange: (name: string) => void; - isDragOver: boolean; - canAcceptDrop: boolean; - visibleItemIds: Set<string>; - children: React.ReactNode; - isNested?: boolean; }
