codeant-ai-for-open-source[bot] commented on code in PR #36239:
URL: https://github.com/apache/superset/pull/36239#discussion_r2741259079
##########
superset/commands/dataset/update.py:
##########
@@ -212,27 +212,26 @@ def _validate_semantics(self, exceptions:
list[ValidationError]) -> None:
valid_uuids: set[UUID] = set()
if metrics:
valid_uuids.update(
- UUID(metric["uuid"]) for metric in metrics if "uuid" in
metric
+ metric["uuid"] for metric in metrics if "uuid" in metric
)
else:
valid_uuids.update(metric.uuid for metric in
self._model.metrics)
if columns:
valid_uuids.update(
- UUID(column["uuid"]) for column in columns if "uuid" in
column
+ column["uuid"] for column in columns if "uuid" in column
Review Comment:
**Suggestion:** Similarly to metrics, if the request explicitly provides an
empty columns list (removing all columns), `valid_uuids` is still built from
the existing model columns because the code tests `if columns` instead of
checking for the presence of the `columns` key, so folders can keep referencing
columns that are being removed without triggering a validation error. [logic
error]
<details>
<summary><b>Severity Level:</b> Critical 🚨</summary>
```mdx
- ❌ Dataset update API accepts removed-column references.
- ⚠️ Folder integrity checks incorrectly pass invalid columns.
```
</details>
```suggestion
if "columns" in self._properties:
columns_prop = self._properties.get("columns") or []
valid_uuids.update(
column["uuid"] for column in columns_prop if "uuid" in
column
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Call PUT /dataset/<pk> handled by superset/datasets/api.py at lines
386-444. The
request body is parsed at datasets/api.py:438 (item =
self.edit_model_schema.load(request.json)) and passed to
UpdateDatasetCommand at
datasets/api.py:443.
2. Provide a payload that explicitly sets "columns": [] and includes a
folders structure
referencing a column UUID that is intended to be removed, e.g. {"columns":
[], "folders":
[{"uuid": "<column-uuid>", "children": []}]}.
3. Inside UpdateDatasetCommand.validate()
(superset/commands/dataset/update.py),
_validate_semantics() is invoked (update.py:202). The code checks columns
with "if
columns:" at update.py:220 and will treat an explicit empty list as falsy.
4. Because an explicit empty list is falsy, the else branch at
update.py:224-225 runs and
populates valid_uuids from self._model.columns, allowing folders to
reference column UUIDs
slated for removal without validation failure.
(Explanation: the current code uses truthiness at update.py:220-225; the
proposed change
checks for presence of the "columns" key so empty-but-present lists are
handled
correctly.)
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/commands/dataset/update.py
**Line:** 220:222
**Comment:**
*Logic Error: Similarly to metrics, if the request explicitly provides
an empty columns list (removing all columns), `valid_uuids` is still built from
the existing model columns because the code tests `if columns` instead of
checking for the presence of the `columns` key, so folders can keep referencing
columns that are being removed without triggering a validation error.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts:
##########
@@ -0,0 +1,631 @@
+/**
+ * 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 { useCallback, useMemo, useRef, useState } from 'react';
+import { t } from '@superset-ui/core';
+import {
+ UniqueIdentifier,
+ DragStartEvent,
+ DragMoveEvent,
+ DragEndEvent,
+ DragOverEvent,
+} from '@dnd-kit/core';
+import { FoldersEditorItemType } from '../../types';
+import {
+ isDefaultFolder,
+ DEFAULT_COLUMNS_FOLDER_UUID,
+ DEFAULT_METRICS_FOLDER_UUID,
+ TreeItem as TreeItemType,
+ FlattenedTreeItem,
+ DRAG_INDENTATION_WIDTH,
+} from '../constants';
+import { buildTree, getProjection, serializeForAPI } from '../treeUtils';
+
+interface UseDragHandlersProps {
+ items: TreeItemType[];
+ setItems: React.Dispatch<React.SetStateAction<TreeItemType[]>>;
+ computeFlattenedItems: (
+ activeId: UniqueIdentifier | null,
+ ) => FlattenedTreeItem[];
+ fullFlattenedItems: FlattenedTreeItem[];
+ selectedItemIds: Set<string>;
+ onChange: (folders: ReturnType<typeof serializeForAPI>) => void;
+ addWarningToast: (message: string) => void;
+}
+
+export function useDragHandlers({
+ items,
+ setItems,
+ computeFlattenedItems,
+ fullFlattenedItems,
+ selectedItemIds,
+ onChange,
+ addWarningToast,
+}: UseDragHandlersProps) {
+ const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
+ const [overId, setOverId] = useState<UniqueIdentifier | null>(null);
+ const [dragOverlayWidth, setDragOverlayWidth] = useState<number |
null>(null);
+ const offsetLeftRef = useRef(0);
+ // Use ref for projectedParentId to avoid re-renders during drag
+ const projectedParentIdRef = useRef<string | null>(null);
+ const [draggedItemIds, setDraggedItemIds] = useState<Set<string>>(new Set());
+
+ const rafIdRef = useRef<number | null>(null);
+
+ // Store the flattened items at drag start to keep them stable during drag
+ // This prevents react-window from re-rendering due to flattenedItems
reference changes
+ const dragStartFlattenedItemsRef = useRef<FlattenedTreeItem[] | null>(null);
+
+ // Update visual drop target indicators via DOM manipulation to avoid
re-renders
+ const updateDropTargetVisuals = useCallback(
+ (newParentId: string | null) => {
+ const oldParentId = projectedParentIdRef.current;
+ if (oldParentId === newParentId) return;
+
+ // Remove highlight from old target
+ if (oldParentId) {
+ const oldElement = document.querySelector(
+ `[data-folder-id="${oldParentId}"]`,
+ );
+ if (oldElement) {
+ oldElement.removeAttribute('data-drop-target');
+ oldElement.removeAttribute('data-forbidden-drop');
+ }
+ }
+
+ // Add highlight to new target
+ if (newParentId) {
+ const newElement = document.querySelector(
+ `[data-folder-id="${newParentId}"]`,
+ );
+ if (newElement) {
+ newElement.setAttribute('data-drop-target', 'true');
+ // Check if it's a forbidden drop target
+ const draggedTypes = new Set<FoldersEditorItemType>();
+ draggedItemIds.forEach((id: string) => {
+ const item = fullFlattenedItems.find(i => i.uuid === id);
+ if (item) draggedTypes.add(item.type);
+ });
+
+ const isForbidden =
+ (newParentId === DEFAULT_COLUMNS_FOLDER_UUID &&
+ draggedTypes.has(FoldersEditorItemType.Metric)) ||
+ (newParentId === DEFAULT_METRICS_FOLDER_UUID &&
+ draggedTypes.has(FoldersEditorItemType.Column)) ||
+ ((newParentId === DEFAULT_COLUMNS_FOLDER_UUID ||
+ newParentId === DEFAULT_METRICS_FOLDER_UUID) &&
+ draggedTypes.has(FoldersEditorItemType.Folder));
+
+ if (isForbidden) {
+ newElement.setAttribute('data-forbidden-drop', 'true');
+ }
+ }
+ }
+
+ projectedParentIdRef.current = newParentId;
+ },
+ [draggedItemIds, fullFlattenedItems],
+ );
+
+ const scheduleProjectedParentUpdate = useCallback(
+ (newParentId: string | null) => {
+ // Only schedule if not already scheduled
+ if (rafIdRef.current === null) {
+ rafIdRef.current = requestAnimationFrame(() => {
+ rafIdRef.current = null;
+ updateDropTargetVisuals(newParentId);
+ });
+ } else {
+ // Update pending value for next frame
+ projectedParentIdRef.current = newParentId;
+ }
+ },
+ [updateDropTargetVisuals],
+ );
+
+ // Compute flattened items, but during drag use the stable snapshot from
drag start
+ // This prevents react-window from re-rendering/re-measuring when
flattenedItems changes
+ const computedFlattenedItems = useMemo(
+ () => computeFlattenedItems(activeId),
+ [computeFlattenedItems, activeId],
+ );
+
+ // Use stable items during drag to prevent scroll jumping
+ // Memoize to avoid creating new array references on every render
+ const flattenedItems = useMemo(
+ () =>
+ activeId && dragStartFlattenedItemsRef.current
+ ? dragStartFlattenedItemsRef.current
+ : computedFlattenedItems,
+ [activeId, computedFlattenedItems],
+ );
+
+ const flattenedItemsIndexMap = useMemo(() => {
+ const map = new Map<string, number>();
+ flattenedItems.forEach((item, index) => {
+ map.set(item.uuid, index);
+ });
+ return map;
+ }, [flattenedItems]);
+
+ const resetDragState = useCallback(() => {
+ setActiveId(null);
+ setOverId(null);
+ offsetLeftRef.current = 0;
+ // Clear visual indicators
+ updateDropTargetVisuals(null);
+ setDraggedItemIds(new Set());
+ setDragOverlayWidth(null);
+ // Clear the stable snapshot so next render uses fresh computed items
+ dragStartFlattenedItemsRef.current = null;
+ if (rafIdRef.current !== null) {
+ cancelAnimationFrame(rafIdRef.current);
+ rafIdRef.current = null;
+ }
+ }, [updateDropTargetVisuals]);
+
+ const handleDragStart = ({ active }: DragStartEvent) => {
+ // Capture the current flattened items BEFORE setting activeId
+ // This ensures the list stays stable during the entire drag operation
+ dragStartFlattenedItemsRef.current = computeFlattenedItems(null);
Review Comment:
**Suggestion:** The stable `flattenedItems` snapshot used during drag is
captured with `computeFlattenedItems(null)`, so it still includes descendants
of the active folder; this allows a folder to be dropped into one of its own
descendants, creating a parent–child cycle (folder becomes child of its child)
so the affected subtree will effectively disappear from the rebuilt tree
structure. Capture the snapshot with the current active id so descendants are
excluded, matching the intended contract of `computeFlattenedItems` and
preventing self-nesting. [logic error]
<details>
<summary><b>Severity Level:</b> Critical 🚨</summary>
```mdx
- ❌ Dataset folders drag-and-drop creates invalid cycles.
- ⚠️ Dataset modal folder structure becomes inconsistent.
- ⚠️ Explore panel shows missing folder subtree entries.
```
</details>
```suggestion
// Use the active id so descendants are excluded and cycles are prevented
dragStartFlattenedItemsRef.current = computeFlattenedItems(active.id);
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Open dataset folders editor UI that uses this hook.
2. Start dragging a folder item. In file
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts
at
lines 183-200 the handler captures a flattened snapshot with
computeFlattenedItems(null).
3. Because computeFlattenedItems(null) includes descendants of the active
folder, the
flattened snapshot still contains the active folder's subtree (verify via
computeFlattenedItems implementation called from this hook).
4. While dragging, drop the active folder onto one of its descendant folders
(the UI
allows hovering over descendants because they exist in the snapshot).
5. On drop, handleDragEnd (lines 267-535) uses fullFlattenedItems and the
projectedPosition computed from the (incorrect) snapshot; the active folder
can become
child of its own descendant, producing a parent-child cycle which then
causes the subtree
to be lost when buildTree(sortedItems) is called (lines 532-534), observable
as missing
metrics/columns in the dataset panel.
6. Expected: computeFlattenedItems(active.id) would exclude descendants
preventing
self-nesting; the suggested change replaces computeFlattenedItems(null) with
computeFlattenedItems(active.id) at the capture point (line 186).
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts
**Line:** 185:186
**Comment:**
*Logic Error: The stable `flattenedItems` snapshot used during drag is
captured with `computeFlattenedItems(null)`, so it still includes descendants
of the active folder; this allows a folder to be dropped into one of its own
descendants, creating a parent–child cycle (folder becomes child of its child)
so the affected subtree will effectively disappear from the rebuilt tree
structure. Capture the snapshot with the current active id so descendants are
excluded, matching the intended contract of `computeFlattenedItems` and
preventing self-nesting.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset/commands/dataset/update.py:
##########
@@ -212,27 +212,26 @@ def _validate_semantics(self, exceptions:
list[ValidationError]) -> None:
valid_uuids: set[UUID] = set()
if metrics:
valid_uuids.update(
- UUID(metric["uuid"]) for metric in metrics if "uuid" in
metric
+ metric["uuid"] for metric in metrics if "uuid" in metric
Review Comment:
**Suggestion:** When the request explicitly provides an empty metrics list
(meaning all metrics are being removed), `valid_uuids` is incorrectly populated
from the existing model metrics because the code uses the truthiness of
`metrics` instead of the presence of the `metrics` key; this allows folders to
reference metrics that are about to be deleted without raising a validation
error. [logic error]
<details>
<summary><b>Severity Level:</b> Critical 🚨</summary>
```mdx
- ❌ Dataset update API accepts removed-metric references.
- ⚠️ Folders may reference deleted metrics silently.
```
</details>
```suggestion
if "metrics" in self._properties:
metrics_prop = self._properties.get("metrics") or []
valid_uuids.update(
metric["uuid"] for metric in metrics_prop if "uuid" in
metric
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Send a PUT request to update a dataset via API endpoint handled at
superset/datasets/api.py:386-444 — specifically the PUT endpoint loads
payload at lines
438-444 and calls UpdateDatasetCommand(pk, item, override_columns).run() at
line 443.
2. Include in the JSON body an explicit empty metrics list and folders that
reference a
metric UUID being removed, e.g. {"metrics": [], "folders": [{"uuid":
"<metric-uuid>",
"children": []}]}. The edit payload is validated by DatasetPutSchema in
datasets/api.py:438 (item = self.edit_model_schema.load(request.json)).
3. Execution enters UpdateDatasetCommand.validate() in
superset/commands/dataset/update.py
(called from UpdateDatasetCommand.run()). The code reaches
_validate_semantics() at
update.py:202-212 and binds metrics with "if metrics :=
self._properties.get(\"metrics\"):" (update.py:208).
4. Because the code uses truthiness (if metrics) rather than presence, an
explicit empty
list (metrics = []) is falsy and the else branch at update.py:217-218 runs,
adding
existing model metric UUIDs (self._model.metrics) into valid_uuids. As a
result, folders
referencing metric UUIDs that the request intends to remove pass validation
and are not
caught.
(Explanation: the current pattern checks truthiness at update.py:213-218;
the suggestion
changes the check to detect an explicit "metrics" key so empty lists are
honored.)
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/commands/dataset/update.py
**Line:** 213:215
**Comment:**
*Logic Error: When the request explicitly provides an empty metrics
list (meaning all metrics are being removed), `valid_uuids` is incorrectly
populated from the existing model metrics because the code uses the truthiness
of `metrics` instead of the presence of the `metrics` key; this allows folders
to reference metrics that are about to be deleted without raising a validation
error.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/components/Datasource/FoldersEditor/index.tsx:
##########
@@ -0,0 +1,429 @@
+/**
+ * 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 { useCallback, useMemo, useState } from 'react';
+import { debounce } from 'lodash';
+import AutoSizer from 'react-virtualized-auto-sizer';
+import {
+ DndContext,
+ DragOverlay,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ UniqueIdentifier,
+} from '@dnd-kit/core';
+import {
+ SortableContext,
+ verticalListSortingStrategy,
+} from '@dnd-kit/sortable';
+import { FoldersEditorItemType } from '../types';
+import { TreeItem as TreeItemType } from './constants';
+import {
+ flattenTree,
+ buildTree,
+ removeChildrenOf,
+ getChildCount,
+ serializeForAPI,
+} from './treeUtils';
+import {
+ createFolder,
+ resetToDefault,
+ ensureDefaultFolders,
+ filterItemsBySearch,
+} from './folderOperations';
+import {
+ pointerSensorOptions,
+ measuringConfig,
+ autoScrollConfig,
+} from './sensors';
+import { FoldersContainer, FoldersContent } from './styles';
+import { FoldersEditorProps } from './types';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { useDragHandlers } from './hooks/useDragHandlers';
+import { useItemHeights } from './hooks/useItemHeights';
+import { useHeightCache } from './hooks/useHeightCache';
+import {
+ FoldersToolbarComponent,
+ ResetConfirmModal,
+ DragOverlayContent,
+} from './components';
+import { VirtualizedTreeList } from './VirtualizedTreeList';
+
+export default function FoldersEditor({
+ folders: initialFolders,
+ metrics,
+ columns,
+ onChange,
+}: FoldersEditorProps) {
+ const { addWarningToast } = useToasts();
+ const itemHeights = useItemHeights();
+ const heightCache = useHeightCache();
+
+ const [items, setItems] = useState<TreeItemType[]>(() => {
+ const ensured = ensureDefaultFolders(initialFolders, metrics, columns);
+ return ensured;
+ });
+
+ const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(
+ new Set(),
+ );
+ const [searchTerm, setSearchTerm] = useState('');
+ const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
+ const [editingFolderId, setEditingFolderId] = useState<string | null>(null);
+ const [showResetConfirm, setShowResetConfirm] = useState(false);
+
+ const sensors = useSensors(useSensor(PointerSensor, pointerSensorOptions));
+
+ const fullFlattenedItems = useMemo(() => flattenTree(items), [items]);
+
+ const collapsedFolderIds = useMemo(
+ () =>
+ fullFlattenedItems.reduce<UniqueIdentifier[]>(
+ (acc, { uuid, type, children }) => {
+ if (
+ type === FoldersEditorItemType.Folder &&
+ collapsedIds.has(uuid) &&
+ children?.length
+ ) {
+ return [...acc, uuid];
+ }
+ return acc;
+ },
+ [],
+ ),
+ [fullFlattenedItems, collapsedIds],
+ );
+
+ const computeFlattenedItems = useCallback(
+ (activeId: UniqueIdentifier | null) =>
+ removeChildrenOf(
+ fullFlattenedItems,
+ activeId != null
+ ? [activeId, ...collapsedFolderIds]
+ : collapsedFolderIds,
+ ),
+ [fullFlattenedItems, collapsedFolderIds],
+ );
+
+ 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],
+ );
+ const columnsMap = useMemo(
+ () => new Map(columns.map(c => [c.uuid, c])),
+ [columns],
+ );
+
+ const {
+ isDragging,
+ activeId,
+ draggedItemIds,
+ dragOverlayWidth,
+ flattenedItems,
+ dragOverlayItems,
+ forbiddenDropFolderIds,
+ handleDragStart,
+ handleDragMove,
+ handleDragOver,
+ handleDragEnd,
+ handleDragCancel,
+ } = useDragHandlers({
+ items,
+ setItems,
+ computeFlattenedItems,
+ fullFlattenedItems,
+ selectedItemIds,
+ onChange,
+ addWarningToast,
+ });
+
+ const debouncedSearch = useCallback(
+ debounce((term: string) => {
+ setSearchTerm(term);
+ }, 300),
+ [],
+ );
+
+ const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
+ debouncedSearch(e.target.value);
Review Comment:
**Suggestion:** The debounced search function can continue to call
`setSearchTerm` after the component has unmounted, because the debounced
callback is never canceled; this can trigger React warnings about state updates
on unmounted components and cause subtle memory leaks. A simple and robust fix
is to remove the debounce here and update `searchTerm` directly in the change
handler. [possible bug]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Console shows React setState-on-unmounted warnings.
- ⚠️ No direct user-facing search failure in dataset modal.
- ⚠️ Developer debugging noise when opening/closing modal quickly.
```
</details>
```suggestion
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Open the Dataset Folders editor UI (component at
superset-frontend/src/components/Datasource/FoldersEditor/index.tsx). The
debounced search
is defined at lines 167-171 and called from handleSearch at lines 174-176.
2. Focus the search input in the Folders tab and type text, which triggers
handleSearch ->
debouncedSearch (call path: handleSearch at line 174 calls debouncedSearch
at line 175).
3. Immediately navigate away or close the dataset modal (unmounting
FoldersEditor) within
the debounce window (300ms). The component unmounts but the lodash debounced
callback is
still scheduled.
4. When the debounced callback runs after 300ms, it calls setSearchTerm at
line 169 on an
unmounted component. React logs a "setState on an unmounted component"
warning in the
console and may retain references until GC, causing a small memory leak and
noisy logs.
5. Expected outcome: React warning appears in console and developer
experience is
degraded; no user-visible functional break, but repeated occurrences during
quick
open/close can clutter logs.
Explanation: The debounced function returned by debounce(...) is never
canceled on unmount
(no cleanup using the debounced function's .cancel()). Therefore scheduled
callbacks can
run after the component has been removed. The code paths and line references
above were
taken from the PR file
superset-frontend/src/components/Datasource/FoldersEditor/index.tsx
(debouncedSearch: lines 167-171; handleSearch call: lines 174-176).
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/components/Datasource/FoldersEditor/index.tsx
**Line:** 167:175
**Comment:**
*Possible Bug: The debounced search function can continue to call
`setSearchTerm` after the component has unmounted, because the debounced
callback is never canceled; this can trigger React warnings about state updates
on unmounted components and cause subtle memory leaks. A simple and robust fix
is to remove the debounce here and update `searchTerm` directly in the change
handler.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts:
##########
@@ -0,0 +1,195 @@
+/**
+ * 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 { styled, css } from '@apache-superset/core/ui';
+
+export const FOLDER_INDENTATION_WIDTH = 24;
+export const ITEM_INDENTATION_WIDTH = 4;
+
+export const TreeItemContainer = styled.div<{
+ depth: number;
+ isDragging: boolean;
+ isOver: boolean;
+ isOverlay?: boolean;
+}>`
+ ${({ theme, depth, isDragging, isOverlay }) => `
+ margin: 0 ${isOverlay ? 0 : theme.marginMD}px;
+ margin-left: ${isOverlay ? 0 : (depth - 1) * FOLDER_INDENTATION_WIDTH +
ITEM_INDENTATION_WIDTH}px;
Review Comment:
**Suggestion:** The left margin for non-folder items is calculated as
`(depth - 1) * FOLDER_INDENTATION_WIDTH + ITEM_INDENTATION_WIDTH`, which
becomes negative when `depth` is 0 (root-level items), causing those items to
render shifted to the left outside their intended container and potentially
misaligning hitboxes and layout; clamping the `(depth - 1)` term to a minimum
of 0 avoids negative indentation while preserving the intended relative
indentation for deeper items. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ❌ Dataset folders UI layout misaligned for root items.
- ⚠️ Hitbox misalignment for folder/item interactions.
- ⚠️ Affects TreeItem rendering in Dataset modal.
```
</details>
```suggestion
margin-left: ${
isOverlay
? 0
: Math.max(depth - 1, 0) * FOLDER_INDENTATION_WIDTH +
ITEM_INDENTATION_WIDTH
}px;
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Start the frontend with the DATASET_FOLDERS feature enabled and open the
dataset modal
(per PR testing instructions).
2. The dataset folders UI renders TreeItem components. The file that renders
the styled
container is
superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx
which imports
TreeItemContainer
from TreeItem.styles.ts (import at TreeItem.tsx:45 — verified via Grep
output).
3. At runtime TreeItem.tsx renders <TreeItemContainer {...containerProps}>
(usage at
TreeItem.tsx:341 as shown by Grep).
The container is supplied a numeric depth prop (prop name verified by the
styled
definition at
superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts:25-30).
4. When a root-level item is rendered with depth = 0, the current expression
in
TreeItem.styles.ts line 33:
(depth - 1) * FOLDER_INDENTATION_WIDTH + ITEM_INDENTATION_WIDTH evaluates
to -1 * 24 +
4 = -20,
producing margin-left: -20px. This causes the element to shift left out
of its intended
container and
may misalign hitboxes and visible layout in the dataset folders UI.
5. Reproduce locally: open Dataset modal → Folders tab → ensure there are
root-level items
(common for top-level metrics/columns).
Observe using browser devtools that the computed margin-left for
TreeItemContainer (see
element styles)
is negative when depth is 0 (inspect element shows margin-left: -20px).
6. The suggested improved code clamps (depth - 1) to minimum 0 via
Math.max(depth - 1, 0)
so depth=0 yields margin-left: ITEM_INDENTATION_WIDTH (4px),
preventing negative indentation and aligning root items correctly.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.styles.ts
**Line:** 33:33
**Comment:**
*Logic Error: The left margin for non-folder items is calculated as
`(depth - 1) * FOLDER_INDENTATION_WIDTH + ITEM_INDENTATION_WIDTH`, which
becomes negative when `depth` is 0 (root-level items), causing those items to
render shifted to the left outside their intended container and potentially
misaligning hitboxes and layout; clamping the `(depth - 1)` term to a minimum
of 0 avoids negative indentation while preserving the intended relative
indentation for deeper items.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts:
##########
@@ -0,0 +1,631 @@
+/**
+ * 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 { useCallback, useMemo, useRef, useState } from 'react';
+import { t } from '@superset-ui/core';
+import {
+ UniqueIdentifier,
+ DragStartEvent,
+ DragMoveEvent,
+ DragEndEvent,
+ DragOverEvent,
+} from '@dnd-kit/core';
+import { FoldersEditorItemType } from '../../types';
+import {
+ isDefaultFolder,
+ DEFAULT_COLUMNS_FOLDER_UUID,
+ DEFAULT_METRICS_FOLDER_UUID,
+ TreeItem as TreeItemType,
+ FlattenedTreeItem,
+ DRAG_INDENTATION_WIDTH,
+} from '../constants';
+import { buildTree, getProjection, serializeForAPI } from '../treeUtils';
+
+interface UseDragHandlersProps {
+ items: TreeItemType[];
+ setItems: React.Dispatch<React.SetStateAction<TreeItemType[]>>;
+ computeFlattenedItems: (
+ activeId: UniqueIdentifier | null,
+ ) => FlattenedTreeItem[];
+ fullFlattenedItems: FlattenedTreeItem[];
+ selectedItemIds: Set<string>;
+ onChange: (folders: ReturnType<typeof serializeForAPI>) => void;
+ addWarningToast: (message: string) => void;
+}
+
+export function useDragHandlers({
+ items,
+ setItems,
+ computeFlattenedItems,
+ fullFlattenedItems,
+ selectedItemIds,
+ onChange,
+ addWarningToast,
+}: UseDragHandlersProps) {
+ const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
+ const [overId, setOverId] = useState<UniqueIdentifier | null>(null);
+ const [dragOverlayWidth, setDragOverlayWidth] = useState<number |
null>(null);
+ const offsetLeftRef = useRef(0);
+ // Use ref for projectedParentId to avoid re-renders during drag
+ const projectedParentIdRef = useRef<string | null>(null);
+ const [draggedItemIds, setDraggedItemIds] = useState<Set<string>>(new Set());
+
+ const rafIdRef = useRef<number | null>(null);
+
+ // Store the flattened items at drag start to keep them stable during drag
+ // This prevents react-window from re-rendering due to flattenedItems
reference changes
+ const dragStartFlattenedItemsRef = useRef<FlattenedTreeItem[] | null>(null);
+
+ // Update visual drop target indicators via DOM manipulation to avoid
re-renders
+ const updateDropTargetVisuals = useCallback(
+ (newParentId: string | null) => {
+ const oldParentId = projectedParentIdRef.current;
+ if (oldParentId === newParentId) return;
+
+ // Remove highlight from old target
+ if (oldParentId) {
+ const oldElement = document.querySelector(
+ `[data-folder-id="${oldParentId}"]`,
+ );
+ if (oldElement) {
+ oldElement.removeAttribute('data-drop-target');
+ oldElement.removeAttribute('data-forbidden-drop');
+ }
+ }
+
+ // Add highlight to new target
+ if (newParentId) {
+ const newElement = document.querySelector(
+ `[data-folder-id="${newParentId}"]`,
+ );
+ if (newElement) {
+ newElement.setAttribute('data-drop-target', 'true');
+ // Check if it's a forbidden drop target
+ const draggedTypes = new Set<FoldersEditorItemType>();
+ draggedItemIds.forEach((id: string) => {
+ const item = fullFlattenedItems.find(i => i.uuid === id);
+ if (item) draggedTypes.add(item.type);
+ });
+
+ const isForbidden =
+ (newParentId === DEFAULT_COLUMNS_FOLDER_UUID &&
+ draggedTypes.has(FoldersEditorItemType.Metric)) ||
+ (newParentId === DEFAULT_METRICS_FOLDER_UUID &&
+ draggedTypes.has(FoldersEditorItemType.Column)) ||
+ ((newParentId === DEFAULT_COLUMNS_FOLDER_UUID ||
+ newParentId === DEFAULT_METRICS_FOLDER_UUID) &&
+ draggedTypes.has(FoldersEditorItemType.Folder));
Review Comment:
**Suggestion:** The forbidden-drop calculation in `updateDropTargetVisuals`
only enforces type rules for default metric/column folders, but it ignores the
rule that default folders themselves cannot be nested into non-default folders;
this leads to inconsistent UX where a non-default folder is visually
highlighted as a valid drop target when dragging a default folder even though
`handleDragEnd` will reject the drop with a warning. Align the forbidden-drop
logic here with the one in `forbiddenDropFolderIds` so any folder that will be
rejected on drop is also marked as a forbidden drop target visually. [logic
error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Visual target highlights contradict drop validation.
- ❌ Users see allowed highlight but drop is rejected.
- ⚠️ UX inconsistency in dataset folders editor.
```
</details>
```suggestion
let hasDraggedDefaultFolder = false;
draggedItemIds.forEach((id: string) => {
const item = fullFlattenedItems.find(i => i.uuid === id);
if (item) {
draggedTypes.add(item.type);
if (
item.type === FoldersEditorItemType.Folder &&
isDefaultFolder(item.uuid)
) {
hasDraggedDefaultFolder = true;
}
}
});
const targetIsDefaultFolder = isDefaultFolder(newParentId);
const isDefaultMetricsFolder =
newParentId === DEFAULT_METRICS_FOLDER_UUID &&
targetIsDefaultFolder;
const isDefaultColumnsFolder =
newParentId === DEFAULT_COLUMNS_FOLDER_UUID &&
targetIsDefaultFolder;
const isForbidden =
// Default folders cannot be nested into non-default folders
(hasDraggedDefaultFolder && !targetIsDefaultFolder) ||
// Default folders cannot contain other folders
((isDefaultMetricsFolder || isDefaultColumnsFolder) &&
draggedTypes.has(FoldersEditorItemType.Folder)) ||
// Metrics/columns type mismatches for default folders
(isDefaultMetricsFolder &&
draggedTypes.has(FoldersEditorItemType.Column)) ||
(isDefaultColumnsFolder &&
draggedTypes.has(FoldersEditorItemType.Metric));
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Open dataset folders editor and begin dragging the default Metrics or
Columns folder
(these UUIDs are referenced at top of the hook: DEFAULT_METRICS_FOLDER_UUID
and
DEFAULT_COLUMNS_FOLDER_UUID).
2. While dragging a default folder, hover a non-default folder target. In
updateDropTargetVisuals (file lines ~76-122, new-target handling at lines
92-119) the code
computes draggedTypes but does not check if a dragged item is a default
folder and whether
the hovered target is non-default.
3. The DOM element receives data-drop-target and is not marked
data-forbidden-drop even
though handleDragEnd (lines 372-380 and lines 551-615 logic in
forbiddenDropFolderIds)
will later reject nesting default folders into non-default folders and show
a warning.
4. Observe the visual highlight indicating a permitted drop, but on mouse up
the hook
rejects the action and shows a warning toast (addWarningToast called at
lines 378-379),
causing inconsistent UX.
5. The suggested improved code adds the same default-folder checks used
elsewhere
(forbiddenDropFolderIds and handleDragEnd) and sets data-forbidden-drop
earlier so the
visual state matches the actual drop validation.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/src/components/Datasource/FoldersEditor/hooks/useDragHandlers.ts
**Line:** 101:113
**Comment:**
*Logic Error: The forbidden-drop calculation in
`updateDropTargetVisuals` only enforces type rules for default metric/column
folders, but it ignores the rule that default folders themselves cannot be
nested into non-default folders; this leads to inconsistent UX where a
non-default folder is visually highlighted as a valid drop target when dragging
a default folder even though `handleDragEnd` will reject the drop with a
warning. Align the forbidden-drop logic here with the one in
`forbiddenDropFolderIds` so any folder that will be rejected on drop is also
marked as a forbidden drop target visually.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx:
##########
@@ -0,0 +1,380 @@
+/**
+ * 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, useState, memo, useMemo } from 'react';
+import { useSortable } from '@dnd-kit/sortable';
+import { useDroppable } from '@dnd-kit/core';
+import { CSS } from '@dnd-kit/utilities';
+import { css } from '@apache-superset/core/ui';
+import { Metric, t } from '@superset-ui/core';
Review Comment:
**Suggestion:** The `metric` prop here is typed using `Metric` from
`@superset-ui/core`, but the rest of the folders editor (e.g.,
`VirtualizedTreeItem` and `FoldersEditor` types) uses `Metric` from
`@superset-ui/chart-controls`, meaning `TreeItem` is assuming a different type
than the actual metric objects being passed; this mismatch can easily lead to
incorrect field assumptions if the two `Metric` definitions diverge, so the
import should be aligned to the same source type as the callers. [type error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Type-check failures in FoldersEditor compilation/tests.
- ⚠️ Developer friction during local builds and CI checks.
- ❌ Potential incorrect field assumptions in TreeItem rendering.
- ⚠️ Inconsistent types across VirtualizedTreeItem and types.ts.
```
</details>
```suggestion
import { t } from '@superset-ui/core';
import type { Metric } from '@superset-ui/chart-controls';
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Open the file that defines the TreeItem component at
superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx and
observe the
import on line 25: `import { Metric, t } from '@superset-ui/core';`.
(Verified via Grep
output showing TreeItem.tsx:25.)
2. Inspect the surrounding Folder editor types and consumers:
superset-frontend/src/components/Datasource/FoldersEditor/types.ts imports
Metric from
@superset-ui/chart-controls (see types.ts:20 in Grep results), and
VirtualizedTreeItem.tsx
also imports type { Metric } from '@superset-ui/chart-controls'
(VirtualizedTreeItem.tsx:23 in Grep results). This indicates callers and
shared types are
using the chart-controls Metric type.
3. Reproduce by running TypeScript checks or the FoldersEditor unit tests:
- From repository root run the TypeScript build or tests that type-check
this area, for
example `yarn build` or `yarn test
src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx`.
4. Expected observable failure: TypeScript may report type incompatibilities
or mismatched
property names when TreeItem's `metric` prop is assumed to match
`@superset-ui/core`'s
Metric but callers provide the `@superset-ui/chart-controls` Metric shape
(for example,
different optional/required fields or different property names used
elsewhere). Concrete
evidence of inconsistent imports was found via Grep: other files in this
feature
(folderValidation.ts, VirtualizedTreeList.tsx, types.ts,
folderOperations.ts, tests)
import Metric from '@superset-ui/chart-controls' while TreeItem imports it
from
'@superset-ui/core' (see Grep output lines referencing those files).
5. If the codebase is compiled or linted in CI, this mismatch can cause type
errors or
masking of real runtime mismatches; aligning the import to `import type {
Metric } from
'@superset-ui/chart-controls'` (as proposed) will match the rest of the
folder editor
callers and avoid the type conflict.
Note: This is not a runtime crash reproduction but a concrete
type-check/test failure
scenario: run the project's TypeScript checker or the FoldersEditor tests to
observe
type-related failures caused by the mismatched Metric import.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/src/components/Datasource/FoldersEditor/TreeItem.tsx
**Line:** 25:25
**Comment:**
*Type Error: The `metric` prop here is typed using `Metric` from
`@superset-ui/core`, but the rest of the folders editor (e.g.,
`VirtualizedTreeItem` and `FoldersEditor` types) uses `Metric` from
`@superset-ui/chart-controls`, meaning `TreeItem` is assuming a different type
than the actual metric objects being passed; this mismatch can easily lead to
incorrect field assumptions if the two `Metric` definitions diverge, so the
import should be aligned to the same source type as the callers.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]