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:
Kamil Gabryjelski
2026-02-27 17:25:06 +01:00
committed by GitHub
parent e5cbc98482
commit 5e890a8cf7
4 changed files with 170 additions and 4 deletions

View File

@@ -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(),
);

View File

@@ -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);
});

View File

@@ -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.

View File

@@ -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);