codeant-ai-for-open-source[bot] commented on code in PR #39461:
URL: https://github.com/apache/superset/pull/39461#discussion_r3429952244


##########
superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx:
##########
@@ -52,18 +52,18 @@ const StyledButtonWrapper = styled.span`
   `}
 `;
 
-type CollectionItem = { id: string | number; [key: string]: any };
+type CollectionItem = { id: string | number; [key: string]: unknown };
 
 function createKeyedCollection(arr: Array<object>) {
   const collectionArray = arr.map(
-    (o: any) =>
+    (o: Record<string, unknown>) =>
       ({
         ...o,
         id: o.id || nanoid(),

Review Comment:
   **Suggestion:** Using a truthy check for `id` treats valid values like `0` 
as missing and replaces them with a random id, which breaks stable identity and 
can cause mismatched updates. Use an explicit null/undefined check instead of 
`||`. [falsy zero check]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Zero-id records get random identities in CollectionTable.
   - ⚠️ Datasource column edits may desync from backend ids.
   - ⚠️ Deletions can target wrong records when id is zero.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. `createKeyedCollection` in
   
`superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx:57-64`
   maps incoming items and assigns `id: o.id || nanoid()`, treating any falsy 
`id` as
   missing.
   
   2. If a caller passes an item with `id` equal to `0` (permitted by
   `CRUDCollectionProps.collection: Record<PropertyKey, any>[]` at
   `superset-frontend/src/components/Datasource/types.ts:47-51` and common for 
numeric
   identifiers), this line replaces `0` with a fresh random `nanoid`.
   
   3. The rest of `CRUDCollection` (lines 77-558) uses `record.id` as the 
stable identifier
   for `rowKey` (around line 540) and for update/delete operations 
(`onCellChange` at 125-155
   and `deleteItem` at 192-211), so the UI and callbacks now refer to a 
synthetic id
   unrelated to the original backend id.
   
   4. Callers like the column and metric tables in `DatasourceEditor`
   
(`superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx`,
   e.g. `ColumnCollectionTable` around 479-575 and `renderMetricCollection` 
around 2100-2150)
   depend on consistent ids in their `collection` arrays; rewriting a 
legitimate `0` id
   breaks identity tracking and can cause updates or deletes to target the 
wrong backend
   record.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=a58a8e6915fb4bf6b2aa0043acb22a0a&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=a58a8e6915fb4bf6b2aa0043acb22a0a&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <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/components/CollectionTable/index.tsx
   **Line:** 62:62
   **Comment:**
        *Falsy Zero Check: Using a truthy check for `id` treats valid values 
like `0` as missing and replaces them with a random id, which breaks stable 
identity and can cause mismatched updates. Use an explicit null/undefined check 
instead of `||`.
   
   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.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=2bceada9953b5296f5866c5b5082a99e6b1b5d9dbd2ffb63202549582e998a7b&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=2bceada9953b5296f5866c5b5082a99e6b1b5d9dbd2ffb63202549582e998a7b&reaction=dislike'>👎</a>



##########
superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx:
##########
@@ -74,270 +74,310 @@ function createKeyedCollection(arr: Array<object>) {
   };
 }
 
-export default class CRUDCollection extends PureComponent<
-  CRUDCollectionProps,
-  CRUDCollectionState
-> {
-  constructor(props: CRUDCollectionProps) {
-    super(props);
-
-    const { collection, collectionArray } = createKeyedCollection(
-      props.collection,
-    );
-
-    // Get initial page size from pagination prop
-    const initialPageSize =
-      typeof props.pagination === 'object' && props.pagination?.pageSize
-        ? props.pagination.pageSize
-        : 10;
-
-    this.state = {
-      expandedColumns: {},
-      collection,
-      collectionArray,
-      sortColumn: '',
-      sort: 0,
-      currentPage: 1,
-      pageSize: initialPageSize,
-    };
-    this.onAddItem = this.onAddItem.bind(this);
-    this.renderExpandableSection = this.renderExpandableSection.bind(this);
-    this.getLabel = this.getLabel.bind(this);
-    this.onFieldsetChange = this.onFieldsetChange.bind(this);
-    this.changeCollection = this.changeCollection.bind(this);
-    this.handleTableChange = this.handleTableChange.bind(this);
-    this.buildTableColumns = this.buildTableColumns.bind(this);
-    this.toggleExpand = this.toggleExpand.bind(this);
-  }
-
-  componentDidUpdate(prevProps: CRUDCollectionProps) {
-    if (this.props.collection !== prevProps.collection) {
-      const { collection, collectionArray } = createKeyedCollection(
-        this.props.collection,
-      );
+export default function CRUDCollection({
+  allowAddItem = false,
+  allowDeletes = false,
+  collection: propsCollection,
+  columnLabels,
+  columnLabelTooltips,
+  emptyMessage = t('No items'),
+  expandFieldset,
+  itemGenerator,
+  itemCellProps,
+  itemRenderers,
+  onChange,
+  tableColumns,
+  sortColumns = [],
+  stickyHeader = false,
+  pagination = false,
+  filterTerm,
+  filterFields,
+}: CRUDCollectionProps) {
+  const [expandedColumns, setExpandedColumns] = useState<
+    Record<PropertyKey, boolean>
+  >({});
+  const [collection, setCollection] = useState<
+    Record<PropertyKey, CollectionItem>
+  >(() => createKeyedCollection(propsCollection).collection);
+  const [collectionArray, setCollectionArray] = useState<CollectionItem[]>(
+    () => createKeyedCollection(propsCollection).collectionArray,
+  );

Review Comment:
   **Suggestion:** `createKeyedCollection` is executed twice during initial 
state setup, which generates two different synthetic `id` sets when incoming 
items do not already have an `id`. That makes `collection` and 
`collectionArray` inconsistent from first render, so updates/deletes by row id 
can target different records. Initialize both states from a single 
`createKeyedCollection` result. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Datasource CollectionTable rows can reappear after deletion.
   - ⚠️ Inline edits may update wrong row in CRUDCollection.
   - ⚠️ Future CollectionTable consumers without ids risk corruption.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. In `createKeyedCollection` at
   
`superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx:57-74`,
   items without an `id` get a synthetic `id` via `nanoid()` (`id: o.id || 
nanoid()`).
   
   2. In `CRUDCollection`'s initial state at lines 99-104 of the same file, 
`useState` for
   `collection` and `collectionArray` each call 
`createKeyedCollection(propsCollection)`
   independently, so for any item lacking `id` two different random ids are 
generated.
   
   3. When a consumer renders `CRUDCollection`/`CollectionTable` with 
`collection` items that
   omit `id` (allowed by `CRUDCollectionProps.collection` at
   `superset-frontend/src/components/Datasource/types.ts:47-51`), `collection` 
(map keyed by
   the first id set) and `collectionArray` (array holding the second id set) 
become
   inconsistent from the very first render.
   
   4. Subsequent operations that use `record.id` from `collectionArray`—for 
example
   `deleteItem` at lines 192-211 or `onFieldsetChange`/`changeCollection` at 
lines 245-255
   and 159-188—act on mismatched keys in `collection`, leading to stale map 
entries,
   reappearing rows, or updates being applied to the wrong logical record.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=47d62d08dcc849808bfdf40b8084e396&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=47d62d08dcc849808bfdf40b8084e396&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <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/components/CollectionTable/index.tsx
   **Line:** 99:104
   **Comment:**
        *Logic Error: `createKeyedCollection` is executed twice during initial 
state setup, which generates two different synthetic `id` sets when incoming 
items do not already have an `id`. That makes `collection` and 
`collectionArray` inconsistent from first render, so updates/deletes by row id 
can target different records. Initialize both states from a single 
`createKeyedCollection` result.
   
   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.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=e6cf09048069949e2b2f6a914af56ddf4c25426121688cc72dd9d2945973c51d&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=e6cf09048069949e2b2f6a914af56ddf4c25426121688cc72dd9d2945973c51d&reaction=dislike'>👎</a>



##########
superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx:
##########
@@ -74,270 +74,310 @@ function createKeyedCollection(arr: Array<object>) {
   };
 }
 
-export default class CRUDCollection extends PureComponent<
-  CRUDCollectionProps,
-  CRUDCollectionState
-> {
-  constructor(props: CRUDCollectionProps) {
-    super(props);
-
-    const { collection, collectionArray } = createKeyedCollection(
-      props.collection,
-    );
-
-    // Get initial page size from pagination prop
-    const initialPageSize =
-      typeof props.pagination === 'object' && props.pagination?.pageSize
-        ? props.pagination.pageSize
-        : 10;
-
-    this.state = {
-      expandedColumns: {},
-      collection,
-      collectionArray,
-      sortColumn: '',
-      sort: 0,
-      currentPage: 1,
-      pageSize: initialPageSize,
-    };
-    this.onAddItem = this.onAddItem.bind(this);
-    this.renderExpandableSection = this.renderExpandableSection.bind(this);
-    this.getLabel = this.getLabel.bind(this);
-    this.onFieldsetChange = this.onFieldsetChange.bind(this);
-    this.changeCollection = this.changeCollection.bind(this);
-    this.handleTableChange = this.handleTableChange.bind(this);
-    this.buildTableColumns = this.buildTableColumns.bind(this);
-    this.toggleExpand = this.toggleExpand.bind(this);
-  }
-
-  componentDidUpdate(prevProps: CRUDCollectionProps) {
-    if (this.props.collection !== prevProps.collection) {
-      const { collection, collectionArray } = createKeyedCollection(
-        this.props.collection,
-      );
+export default function CRUDCollection({
+  allowAddItem = false,
+  allowDeletes = false,
+  collection: propsCollection,
+  columnLabels,
+  columnLabelTooltips,
+  emptyMessage = t('No items'),
+  expandFieldset,
+  itemGenerator,
+  itemCellProps,
+  itemRenderers,
+  onChange,
+  tableColumns,
+  sortColumns = [],
+  stickyHeader = false,
+  pagination = false,
+  filterTerm,
+  filterFields,
+}: CRUDCollectionProps) {
+  const [expandedColumns, setExpandedColumns] = useState<
+    Record<PropertyKey, boolean>
+  >({});
+  const [collection, setCollection] = useState<
+    Record<PropertyKey, CollectionItem>
+  >(() => createKeyedCollection(propsCollection).collection);
+  const [collectionArray, setCollectionArray] = useState<CollectionItem[]>(
+    () => createKeyedCollection(propsCollection).collectionArray,
+  );
+  const [sortColumn, setSortColumn] = useState<string>('');
+  const [sort, setSort] = useState<SortOrderEnum>(SortOrderEnum.Unsorted);
+  // Controlled pagination: tracked so that filtering can clamp currentPage
+  // back to a valid page (avoids the user being stranded on an empty page
+  // when filterTerm shrinks the result set).
+  const [pageSize, setPageSize] = useState<number>(() =>
+    typeof pagination === 'object' && pagination?.pageSize
+      ? pagination.pageSize
+      : 10,
+  );
+  const [currentPage, setCurrentPage] = useState<number>(1);
+
+  // Sync with props.collection changes
+  useEffect(() => {
+    const { collection: newCollection, collectionArray: newCollectionArray } =
+      createKeyedCollection(propsCollection);
+    setCollection(newCollection);
+    setCollectionArray(newCollectionArray);
+  }, [propsCollection]);
+
+  const onCellChange = useCallback(
+    (id: string | number, col: string, val: unknown) => {
+      setCollection(prevCollection => {
+        const updatedCollection = {
+          ...prevCollection,
+          [id]: {
+            ...prevCollection[id],
+            [col]: val,
+          },
+        };
+        return updatedCollection;
+      });
 
-      this.setState(prevState => ({
-        collection,
-        collectionArray,
-        expandedColumns: prevState.expandedColumns,
-      }));
-    }
-  }
-
-  onCellChange(id: string | number, col: string, val: unknown) {
-    this.setState(prevState => {
-      const updatedCollection = {
-        ...prevState.collection,
-        [id]: {
-          ...prevState.collection[id],
-          [col]: val,
-        },
-      };
-      const updatedCollectionArray = prevState.collectionArray.map(item =>
-        item.id === id ? updatedCollection[id] : item,
-      );
+      setCollectionArray(prevCollectionArray => {
+        const updatedCollectionArray = prevCollectionArray.map(item => {
+          if (item.id === id) {
+            return {
+              ...item,
+              [col]: val,
+            };
+          }
+          return item;
+        });
+
+        if (onChange) {
+          onChange(updatedCollectionArray);
+        }
+
+        return updatedCollectionArray;
+      });
+    },
+    [onChange],
+  );
 
-      if (this.props.onChange) {
-        this.props.onChange(updatedCollectionArray);
+  const changeCollection = useCallback(
+    (
+      newCollection: Record<PropertyKey, CollectionItem>,
+      currentCollectionArray: CollectionItem[],
+    ) => {
+      // Preserve existing order instead of recreating from Object.keys()
+      const existingIds = new Set(currentCollectionArray.map(item => item.id));
+      const newCollectionArray: CollectionItem[] = [];
+
+      // First pass: preserve existing order and update items
+      for (const existingItem of currentCollectionArray) {
+        if (newCollection[existingItem.id]) {
+          newCollectionArray.push(newCollection[existingItem.id]);
+        }
       }
-      return {
-        collection: updatedCollection,
-        collectionArray: updatedCollectionArray,
-      };
-    });
-  }
 
-  onAddItem() {
-    if (this.props.itemGenerator) {
-      let newItem = this.props.itemGenerator();
+      // Second pass: add new items
+      for (const item of Object.values(newCollection)) {
+        if (!existingIds.has(item.id)) {
+          newCollectionArray.push(item);
+        }
+      }
+
+      setCollection(newCollection);
+      setCollectionArray(newCollectionArray);
+
+      if (onChange) {
+        onChange(newCollectionArray);
+      }
+    },
+    [onChange],
+  );
+
+  const deleteItem = useCallback(
+    (id: string | number) => {
+      setCollection(prevCollection => {
+        const newColl = { ...prevCollection };
+        delete newColl[id];
+        return newColl;
+      });
+
+      setCollectionArray(prevCollectionArray => {
+        const newCollectionArray = prevCollectionArray.filter(
+          item => item.id !== id,
+        );
+
+        if (onChange) {
+          onChange(newCollectionArray);
+        }
+
+        return newCollectionArray;
+      });
+    },
+    [onChange],
+  );
+
+  const onAddItem = useCallback(() => {
+    if (itemGenerator) {
+      let newItem = itemGenerator() as CollectionItem;
       const shouldStartExpanded = newItem.expanded === true;
       if (!newItem.id) {
         newItem = { ...newItem, id: nanoid() };
       }
       delete newItem.expanded;
 
-      this.setState(
-        prevState => {
-          const newCollection = {
-            ...prevState.collection,
-            [newItem.id]: newItem,
-          };
-          const newExpandedColumns = shouldStartExpanded
-            ? { ...prevState.expandedColumns, [newItem.id]: true }
-            : prevState.expandedColumns;
-          const newCollectionArray = [newItem, ...prevState.collectionArray];
-
-          return {
-            collection: newCollection,
-            collectionArray: newCollectionArray,
-            expandedColumns: newExpandedColumns,
-          };
-        },
-        () => {
-          if (this.props.onChange) {
-            this.props.onChange(this.state.collectionArray);
-          }
-        },
-      );
-    }
-  }
+      setCollection(prevCollection => ({
+        ...prevCollection,
+        [newItem.id]: newItem,
+      }));
 
-  onFieldsetChange(item: any) {
-    this.changeCollection({
-      ...this.state.collection,
-      [item.id]: item,
-    });
-  }
+      setCollectionArray(prevCollectionArray => {
+        const newCollectionArray = [newItem, ...prevCollectionArray];
 
-  getLabel(col: any): string {
-    const { columnLabels } = this.props;
-    let label = columnLabels?.[col] ? columnLabels[col] : col;
-    if (label.startsWith('__')) {
-      label = '';
-    }
-    return label;
-  }
-
-  getTooltip(col: string): string | undefined {
-    const { columnLabelTooltips } = this.props;
-    return columnLabelTooltips?.[col];
-  }
-
-  changeCollection(collection: any) {
-    // Preserve existing order instead of recreating from Object.keys()
-    const existingIds = new Set(
-      this.state.collectionArray.map(item => item.id),
-    );
-    const newCollectionArray: CollectionItem[] = [];
-
-    // First pass: preserve existing order and update items
-    for (const existingItem of this.state.collectionArray) {
-      if (collection[existingItem.id]) {
-        newCollectionArray.push(collection[existingItem.id]);
+        if (onChange) {
+          onChange(newCollectionArray);
+        }
+
+        return newCollectionArray;
+      });
+
+      if (shouldStartExpanded) {
+        setExpandedColumns(prev => ({ ...prev, [newItem.id]: true }));
       }
     }
+  }, [itemGenerator, onChange]);
+
+  const onFieldsetChange = useCallback(
+    (item: CollectionItem) => {
+      changeCollection(
+        {
+          ...collection,
+          [item.id]: item,
+        },
+        collectionArray,
+      );
+    },
+    [changeCollection, collection, collectionArray],
+  );
 
-    // Second pass: add new items
-    for (const item of Object.values(collection) as CollectionItem[]) {
-      if (!existingIds.has(item.id)) {
-        newCollectionArray.push(item);
+  const getLabel = useCallback(
+    (col: string): string => {
+      let label = columnLabels?.[col] ? columnLabels[col] : col;
+      if (label.startsWith('__')) {
+        label = '';
       }
-    }
+      return label;
+    },
+    [columnLabels],
+  );
 
-    this.setState({ collection, collectionArray: newCollectionArray });
+  const getTooltip = useCallback(
+    (col: string): string | undefined => columnLabelTooltips?.[col],
+    [columnLabelTooltips],
+  );
 
-    if (this.props.onChange) {
-      this.props.onChange(newCollectionArray);
-    }
-  }
-
-  deleteItem(id: string | number) {
-    const newColl = { ...this.state.collection };
-    delete newColl[id];
-    this.changeCollection(newColl);
-  }
-
-  toggleExpand(id: any) {
-    this.setState(prevState => ({
-      expandedColumns: {
-        ...prevState.expandedColumns,
-        [id]: !prevState.expandedColumns[id],
-      },
+  const toggleExpand = useCallback((id: string | number) => {
+    setExpandedColumns(prev => ({
+      ...prev,
+      [id]: !prev[id],
     }));
-  }
-
-  handleTableChange(
-    pagination: TablePaginationConfig,
-    _filters: Record<string, FilterValue | null>,
-    sorter: SorterResult<CollectionItem> | SorterResult<CollectionItem>[],
-  ) {
-    // Handle pagination changes
-    if (pagination.current !== undefined && pagination.pageSize !== undefined) 
{
-      this.setState({
-        currentPage: pagination.current,
-        pageSize: pagination.pageSize,
-      });
-    }
-
-    // Handle sorting changes
-    const columnSorter = Array.isArray(sorter) ? sorter[0] : sorter;
-    let newSortColumn = '';
-    let newSortOrder = 0;
-
-    if (columnSorter?.columnKey && columnSorter?.order) {
-      newSortColumn = columnSorter.columnKey as string;
-      newSortOrder = columnSorter.order === 'ascend' ? 1 : 2;
-    }
-
-    const { sortColumns } = this.props;
-    const col = newSortColumn;
+  }, []);
+
+  const handleTableChange = useCallback(
+    (
+      paginationEvt: TablePaginationConfig,
+      _filters: Record<string, FilterValue | null>,
+      sorter: SorterResult<CollectionItem> | SorterResult<CollectionItem>[],
+    ) => {
+      if (
+        paginationEvt.current !== undefined &&
+        paginationEvt.pageSize !== undefined
+      ) {
+        setCurrentPage(paginationEvt.current);
+        setPageSize(paginationEvt.pageSize);
+      }
+      const columnSorter = Array.isArray(sorter) ? sorter[0] : sorter;
+      let newSortColumn = '';
+      let newSortOrder = SortOrderEnum.Unsorted;
+
+      if (columnSorter?.columnKey && columnSorter?.order) {
+        newSortColumn = columnSorter.columnKey as string;
+        newSortOrder =
+          columnSorter.order === 'ascend'
+            ? SortOrderEnum.Asc
+            : SortOrderEnum.Desc;
+      }
 
-    if (sortColumns?.includes(col) || newSortOrder === 0) {
-      let sortedArray = [...this.props.collection];
+      const col = newSortColumn;
+
+      if (
+        sortColumns?.includes(col) ||
+        newSortOrder === SortOrderEnum.Unsorted
+      ) {
+        let sortedArray = [...propsCollection] as CollectionItem[];

Review Comment:
   **Suggestion:** Sorting starts from `propsCollection` instead of the 
component's current `collectionArray`, so local edits/additions/deletions can 
be discarded when the user sorts (especially when `onChange` is not controlling 
props immediately). Sort the current state data source to preserve in-session 
mutations. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Datasource CollectionTable edits can be lost after sorting.
   - ⚠️ Metrics table rows may revert when user sorts columns.
   - ⚠️ Spatial configuration edits can disappear on sort actions.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. In `CRUDCollection`'s `handleTableChange` handler at
   
`superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx:281-348`,
   sorting logic starts by cloning `propsCollection` (`let sortedArray = 
[...propsCollection]
   as CollectionItem[];`) instead of the current `collectionArray` state.
   
   2. Earlier in the same component, user edits and additions update only 
internal state:
   `onCellChange` at lines 125-155 and `onAddItem` at 215-243 mutate 
`collection` /
   `collectionArray` and optionally call `onChange`, but 
`CRUDCollectionProps.onChange` is
   optional (`superset-frontend/src/components/Datasource/types.ts:77`) and 
callers may
   choose not to immediately rebind `collection` props.
   
   3. In such a partially- or uncontrolled usage, a user can add or edit rows 
(stateful
   changes in `collectionArray` only), then click a sortable column header 
configured via
   `sortColumns` (e.g., column/metric/spatial tables in `DatasourceEditor.tsx` 
around
   496-575, 2100-2150, and 2316-2338).
   
   4. When the sort event fires, `handleTableChange` rebuilds `sortedArray` 
from the stale
   `propsCollection`, discarding any in-session state-only changes, so new or 
edited rows
   disappear or revert as soon as the user sorts the table.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=96e2008ce5ba45acbc18e7472cf7c85b&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=96e2008ce5ba45acbc18e7472cf7c85b&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <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/components/CollectionTable/index.tsx
   **Line:** 312:312
   **Comment:**
        *Logic Error: Sorting starts from `propsCollection` instead of the 
component's current `collectionArray`, so local edits/additions/deletions can 
be discarded when the user sorts (especially when `onChange` is not controlling 
props immediately). Sort the current state data source to preserve in-session 
mutations.
   
   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.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=b6a057ab0dad5f0ae36adf011b593f46e1c98fe0584e2d8ec947f6312c61af04&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=b6a057ab0dad5f0ae36adf011b593f46e1c98fe0584e2d8ec947f6312c61af04&reaction=dislike'>👎</a>



-- 
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]

Reply via email to