mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +00:00
fix(folders): remove stale column/metric refs from folders on delete (#38302)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
e5cbc98482
commit
5e890a8cf7
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { DatasourceFolder } from 'src/explore/components/DatasourcePanel/types';
|
||||
import { FoldersEditorItemType } from '../types';
|
||||
import { TreeItem as TreeItemType } from './constants';
|
||||
import {
|
||||
@@ -39,6 +40,7 @@ import {
|
||||
buildTree,
|
||||
removeChildrenOf,
|
||||
serializeForAPI,
|
||||
filterFoldersByValidUuids,
|
||||
} from './treeUtils';
|
||||
import {
|
||||
createFolder,
|
||||
@@ -80,6 +82,21 @@ export default function FoldersEditor({
|
||||
return ensured;
|
||||
});
|
||||
|
||||
// Sync folders when columns/metrics are removed externally
|
||||
useEffect(() => {
|
||||
const validUuids = new Set<string>();
|
||||
columns.forEach(c => {
|
||||
if (c.uuid) validUuids.add(c.uuid);
|
||||
});
|
||||
metrics.forEach(m => {
|
||||
if (m.uuid) validUuids.add(m.uuid);
|
||||
});
|
||||
|
||||
setItems(prevItems =>
|
||||
filterFoldersByValidUuids(prevItems as DatasourceFolder[], validUuids),
|
||||
);
|
||||
}, [columns, metrics]);
|
||||
|
||||
const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
serializeForAPI,
|
||||
getProjection,
|
||||
countAllFolders,
|
||||
filterFoldersByValidUuids,
|
||||
} from './treeUtils';
|
||||
import { FoldersEditorItemType } from '../types';
|
||||
import { DatasourceFolder } from 'src/explore/components/DatasourcePanel/types';
|
||||
@@ -726,3 +727,86 @@ test('countAllFolders ignores non-folder children', () => {
|
||||
|
||||
expect(countAllFolders(folders)).toBe(1);
|
||||
});
|
||||
|
||||
test('filterFoldersByValidUuids removes items with invalid UUIDs', () => {
|
||||
const folders: DatasourceFolder[] = [
|
||||
createFolderItem('f1', 'Metrics', [
|
||||
createMetricItem('m1', 'Metric 1'),
|
||||
createMetricItem('m2', 'Metric 2'),
|
||||
]),
|
||||
] as DatasourceFolder[];
|
||||
|
||||
const validUuids = new Set(['m1']);
|
||||
const filtered = filterFoldersByValidUuids(folders, validUuids);
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].children).toHaveLength(1);
|
||||
expect(filtered[0].children![0].uuid).toBe('m1');
|
||||
});
|
||||
|
||||
test('filterFoldersByValidUuids preserves folders even when empty', () => {
|
||||
const folders: DatasourceFolder[] = [
|
||||
createFolderItem('f1', 'Metrics', [createMetricItem('m1', 'Metric 1')]),
|
||||
] as DatasourceFolder[];
|
||||
|
||||
const validUuids = new Set<string>();
|
||||
const filtered = filterFoldersByValidUuids(folders, validUuids);
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].uuid).toBe('f1');
|
||||
expect(filtered[0].children).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('filterFoldersByValidUuids handles nested folders', () => {
|
||||
const folders: DatasourceFolder[] = [
|
||||
createFolderItem('f1', 'Root', [
|
||||
createFolderItem('f2', 'Nested', [
|
||||
createMetricItem('m1', 'Metric 1'),
|
||||
createColumnItem('c1', 'Column 1'),
|
||||
]),
|
||||
createColumnItem('c2', 'Column 2'),
|
||||
]),
|
||||
] as DatasourceFolder[];
|
||||
|
||||
const validUuids = new Set(['m1', 'c2']);
|
||||
const filtered = filterFoldersByValidUuids(folders, validUuids);
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].children).toHaveLength(2);
|
||||
|
||||
const nestedFolder = filtered[0].children![0] as DatasourceFolder;
|
||||
expect(nestedFolder.uuid).toBe('f2');
|
||||
expect(nestedFolder.children).toHaveLength(1);
|
||||
expect(nestedFolder.children![0].uuid).toBe('m1');
|
||||
|
||||
expect(filtered[0].children![1].uuid).toBe('c2');
|
||||
});
|
||||
|
||||
test('filterFoldersByValidUuids keeps all items when all UUIDs are valid', () => {
|
||||
const folders: DatasourceFolder[] = [
|
||||
createFolderItem('f1', 'Metrics', [
|
||||
createMetricItem('m1', 'Metric 1'),
|
||||
createMetricItem('m2', 'Metric 2'),
|
||||
]),
|
||||
] as DatasourceFolder[];
|
||||
|
||||
const validUuids = new Set(['m1', 'm2']);
|
||||
const filtered = filterFoldersByValidUuids(folders, validUuids);
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].children).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('filterFoldersByValidUuids returns same reference when nothing changed', () => {
|
||||
const folders: DatasourceFolder[] = [
|
||||
createFolderItem('f1', 'Root', [
|
||||
createFolderItem('f2', 'Nested', [createMetricItem('m1', 'Metric 1')]),
|
||||
createColumnItem('c1', 'Column 1'),
|
||||
]),
|
||||
] as DatasourceFolder[];
|
||||
|
||||
const validUuids = new Set(['m1', 'c1']);
|
||||
const filtered = filterFoldersByValidUuids(folders, validUuids);
|
||||
|
||||
expect(filtered).toBe(folders);
|
||||
});
|
||||
|
||||
@@ -331,6 +331,52 @@ export function serializeForAPI(items: TreeItem[]): DatasourceFolder[] {
|
||||
.filter((folder): folder is DatasourceFolder => folder !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove leaf items whose UUIDs are not in the valid set.
|
||||
* Returns the original reference when nothing was removed.
|
||||
*/
|
||||
export function filterFoldersByValidUuids(
|
||||
folders: DatasourceFolder[],
|
||||
validUuids: Set<string>,
|
||||
): DatasourceFolder[] {
|
||||
const filterChildren = (
|
||||
children: (DatasourceFolder | DatasourceFolderItem)[] | undefined,
|
||||
): (DatasourceFolder | DatasourceFolderItem)[] | undefined => {
|
||||
if (!children) return children;
|
||||
|
||||
let childChanged = false;
|
||||
const result: (DatasourceFolder | DatasourceFolderItem)[] = [];
|
||||
for (const child of children) {
|
||||
if (child.type === FoldersEditorItemType.Folder && 'children' in child) {
|
||||
const filtered = filterChildren((child as DatasourceFolder).children);
|
||||
if (filtered !== (child as DatasourceFolder).children) {
|
||||
childChanged = true;
|
||||
result.push({ ...child, children: filtered } as DatasourceFolder);
|
||||
} else {
|
||||
result.push(child);
|
||||
}
|
||||
} else if (validUuids.has(child.uuid)) {
|
||||
result.push(child);
|
||||
} else {
|
||||
childChanged = true;
|
||||
}
|
||||
}
|
||||
return childChanged ? result : children;
|
||||
};
|
||||
|
||||
let changed = false;
|
||||
const result = folders.map(folder => {
|
||||
const filtered = filterChildren(folder.children);
|
||||
if (filtered !== folder.children) {
|
||||
changed = true;
|
||||
return { ...folder, children: filtered };
|
||||
}
|
||||
return folder;
|
||||
});
|
||||
|
||||
return changed ? result : folders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively counts all folders in a DatasourceFolder array,
|
||||
* including nested sub-folders within children.
|
||||
|
||||
@@ -95,7 +95,10 @@ import {
|
||||
isDefaultFolder,
|
||||
} from '../../FoldersEditor/constants';
|
||||
import { validateFolders } from '../../FoldersEditor/folderValidation';
|
||||
import { countAllFolders } from '../../FoldersEditor/treeUtils';
|
||||
import {
|
||||
countAllFolders,
|
||||
filterFoldersByValidUuids,
|
||||
} from '../../FoldersEditor/treeUtils';
|
||||
import FoldersEditor from '../../FoldersEditor';
|
||||
import { DatasourceFolder } from 'src/explore/components/DatasourcePanel/types';
|
||||
|
||||
@@ -135,6 +138,7 @@ interface Metric {
|
||||
|
||||
interface Column {
|
||||
id?: number;
|
||||
uuid?: string;
|
||||
column_name: string;
|
||||
verbose_name?: string;
|
||||
description?: string;
|
||||
@@ -992,11 +996,26 @@ class DatasourceEditor extends PureComponent<
|
||||
const sql =
|
||||
datasourceType === DATASOURCE_TYPES.physical.key ? '' : datasource.sql;
|
||||
|
||||
const columns = [
|
||||
...this.state.databaseColumns,
|
||||
...this.state.calculatedColumns,
|
||||
];
|
||||
|
||||
// Remove deleted column/metric references from folders
|
||||
const validUuids = new Set<string>();
|
||||
for (const col of columns) {
|
||||
if (col.uuid) validUuids.add(col.uuid);
|
||||
}
|
||||
for (const metric of datasource.metrics ?? []) {
|
||||
if (metric.uuid) validUuids.add(metric.uuid);
|
||||
}
|
||||
const folders = filterFoldersByValidUuids(this.state.folders, validUuids);
|
||||
|
||||
const newDatasource = {
|
||||
...this.state.datasource,
|
||||
sql,
|
||||
columns: [...this.state.databaseColumns, ...this.state.calculatedColumns],
|
||||
folders: this.state.folders,
|
||||
columns,
|
||||
folders,
|
||||
};
|
||||
|
||||
this.props.onChange?.(newDatasource, this.state.errors);
|
||||
|
||||
Reference in New Issue
Block a user